Revision c53aa229
b/trunk/Pithos.Client.WPF/Pithos.Client.WPF.csproj | ||
---|---|---|
121 | 121 |
</Reference> |
122 | 122 |
<Reference Include="System.Drawing" /> |
123 | 123 |
<Reference Include="System.Runtime.Serialization" /> |
124 |
<Reference Include="System.ServiceModel" /> |
|
124 | 125 |
<Reference Include="System.Windows.Forms" /> |
125 | 126 |
<Reference Include="System.Windows.Interactivity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> |
126 | 127 |
<HintPath>..\packages\Caliburn.Micro.1.2.0\lib\Net40\System.Windows.Interactivity.dll</HintPath> |
... | ... | |
161 | 162 |
<DependentUpon>PreferencesView.xaml</DependentUpon> |
162 | 163 |
</Compile> |
163 | 164 |
<Compile Include="PreferencesViewModel.cs" /> |
164 |
<Compile Include="TaskbarViewModel.cs" /> |
|
165 |
<Compile Include="Services\Events.cs" /> |
|
166 |
<Compile Include="ShellViewModel.cs" /> |
|
167 |
<Compile Include="Services\StatusService.cs" /> |
|
165 | 168 |
<Compile Include="Wpf32Window.cs" /> |
166 | 169 |
<Page Include="FilePropertiesView.xaml"> |
167 | 170 |
<SubType>Designer</SubType> |
... | ... | |
189 | 192 |
<SubType>Designer</SubType> |
190 | 193 |
<Generator>MSBuild:Compile</Generator> |
191 | 194 |
</Page> |
195 |
<Page Include="ShellView.xaml"> |
|
196 |
<Generator>MSBuild:Compile</Generator> |
|
197 |
<SubType>Designer</SubType> |
|
198 |
</Page> |
|
192 | 199 |
</ItemGroup> |
193 | 200 |
<ItemGroup> |
194 | 201 |
<Compile Include="Properties\AssemblyInfo.cs"> |
b/trunk/Pithos.Client.WPF/PreferencesView.xaml | ||
---|---|---|
31 | 31 |
<RowDefinition Height="Auto"/> |
32 | 32 |
</Grid.RowDefinitions> |
33 | 33 |
|
34 |
<tb:TaskbarIcon x:Name="TaskbarView" |
|
35 |
IconSource="{Binding StatusIcon}" |
|
36 |
ToolTipText="{Binding StatusMessage}" |
|
37 |
DataContext="{Binding Taskbar}" |
|
38 |
MenuActivation="LeftOrRightClick" |
|
39 |
DoubleClickCommand="{Binding DataContext.Taskbar.OpenPithosFolderCommand,ElementName=TheView}" |
|
40 |
cal:Bind.Model="{Binding DataContext.Taskbar,ElementName=TheView}"> |
|
41 |
<tb:TaskbarIcon.ContextMenu> |
|
42 |
<ContextMenu x:Name="TaskbarMenu" > |
|
43 |
<MenuItem Header="Open PITHOS Folder" x:Name="OpenPithosFolder" cal:Message.Attach="OpenPithosFolder" FontWeight="Bold" /> |
|
44 |
<MenuItem Header="Launch PITHOS Site" x:Name="GoToSite" cal:Message.Attach="GoToSite" /> |
|
45 |
<MenuItem Header="Recently Changed Files" x:Name="Taskbar_RecentFiles" ItemsSource="{Binding RecentFiles}"> |
|
46 |
<MenuItem.ItemTemplate> |
|
47 |
<DataTemplate> |
|
48 |
<TextBlock Text="{Binding FileName}"/> |
|
49 |
</DataTemplate> |
|
50 |
</MenuItem.ItemTemplate> |
|
51 |
</MenuItem> |
|
52 |
<Separator /> |
|
53 |
<MenuItem Header="{Binding UsageMessage}" x:Name="UsageMessage" /> |
|
54 |
<Separator /> |
|
55 |
<MenuItem Header="{Binding StatusMessage}" x:Name="StatusMessage" /> |
|
56 |
<Separator /> |
|
57 |
<MenuItem Header="{Binding PauseSyncCaption}" x:Name="ToggleSynching" cal:Message.Attach="ToggleSynching" /> |
|
58 |
<Separator /> |
|
59 |
<MenuItem Header="Preferences ..." x:Name="ShowPreferences" Click="ShowPreferences_Click" cal:Message.Attach="ShowPreferences" /> |
|
60 |
<Separator /> |
|
61 |
<MenuItem Header="Exit" Name="ExitPithos" cal:Message.Attach="ExitPithos" /> |
|
62 |
</ContextMenu> |
|
63 |
</tb:TaskbarIcon.ContextMenu> |
|
64 |
</tb:TaskbarIcon> |
|
65 |
|
|
66 |
|
|
67 |
|
|
34 |
|
|
68 | 35 |
<TabControl Grid.Row="0"> |
69 | 36 |
<TabItem VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"> |
70 | 37 |
<TabItem.Header> |
... | ... | |
216 | 183 |
</TabControl> |
217 | 184 |
|
218 | 185 |
<StackPanel Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Right"> |
219 |
<Button Name="SaveChanges" Content="OK" Click="SaveChanges_Click" Margin="5,5,10,5" Style="{StaticResource ButtonStyle}"/>
|
|
220 |
<Button Name="RejectChanges" Content="Cancel" Click="RejectChanges_Click" Margin="5,5,10,5" Style="{StaticResource ButtonStyle}"/>
|
|
186 |
<Button Name="SaveChanges" Content="OK" Margin="5,5,10,5" Style="{StaticResource ButtonStyle}"/> |
|
187 |
<Button Name="RejectChanges" Content="Cancel" Margin="5,5,10,5" Style="{StaticResource ButtonStyle}"/> |
|
221 | 188 |
<Button Name="ApplyChanges" Content="Apply" Style="{StaticResource ButtonStyle}" /> |
222 | 189 |
</StackPanel> |
223 | 190 |
</Grid> |
b/trunk/Pithos.Client.WPF/PreferencesView.xaml.cs | ||
---|---|---|
25 | 25 |
{ |
26 | 26 |
InitializeComponent(); |
27 | 27 |
} |
28 |
|
|
29 |
private void SaveChanges_Click(object sender, RoutedEventArgs e) |
|
30 |
{ |
|
31 |
this.Hide(); |
|
32 |
} |
|
33 |
|
|
34 |
private void RejectChanges_Click(object sender, RoutedEventArgs e) |
|
35 |
{ |
|
36 |
this.Hide(); |
|
37 |
} |
|
38 |
|
|
39 |
private void ShowPreferences_Click(object sender, RoutedEventArgs e) |
|
40 |
{ |
|
41 |
this.Show(); |
|
42 |
this.Topmost = true; |
|
43 |
|
|
44 |
|
|
45 |
} |
|
46 |
|
|
47 | 28 |
|
48 | 29 |
} |
49 | 30 |
} |
b/trunk/Pithos.Client.WPF/PreferencesViewModel.cs | ||
---|---|---|
36 | 36 |
/// <summary> |
37 | 37 |
/// TODO: Update summary. |
38 | 38 |
/// </summary> |
39 |
[Export(typeof(IShell))]
|
|
40 |
public class PreferencesViewModel : Screen, IShell, IHandle<Notification>
|
|
39 |
[Export] |
|
40 |
public class PreferencesViewModel : Screen |
|
41 | 41 |
{ |
42 | 42 |
private IEventAggregator _events; |
43 | 43 |
|
... | ... | |
49 | 49 |
set |
50 | 50 |
{ |
51 | 51 |
_settings = value; |
52 |
foreach (var account in _settings.Accounts.Where(account=>account.IsActive)) |
|
53 |
{ |
|
54 |
if (String.IsNullOrWhiteSpace(account.RootPath)) |
|
55 |
{ |
|
56 |
account.RootPath = _settings.PithosPath; |
|
57 |
_settings.Save(); |
|
58 |
} |
|
59 |
} |
|
60 | 52 |
NotifyOfPropertyChange(()=>Settings); |
61 | 53 |
} |
62 | 54 |
} |
63 |
|
|
64 |
|
|
65 |
private Dictionary<string,PithosMonitor> _monitors=new Dictionary<string, PithosMonitor>(); |
|
66 |
public Dictionary<string, PithosMonitor> Monitors |
|
67 |
{ |
|
68 |
get { return _monitors; } |
|
69 |
} |
|
70 |
|
|
71 |
private TaskbarViewModel _taskbar; |
|
72 |
public TaskbarViewModel Taskbar |
|
73 |
{ |
|
74 |
get { return _taskbar; } |
|
75 |
set |
|
76 |
{ |
|
77 |
_taskbar = value; |
|
78 |
} |
|
79 |
} |
|
80 |
|
|
55 |
|
|
56 |
public ShellViewModel Shell { get; set; } |
|
81 | 57 |
//ShellExtensionController _extensionController=new ShellExtensionController(); |
82 |
|
|
83 |
[ImportingConstructor] |
|
84 |
public PreferencesViewModel(IEventAggregator events, TaskbarViewModel taskbar, PithosSettings settings) |
|
58 |
|
|
59 |
public PreferencesViewModel(IEventAggregator events,ShellViewModel shell, PithosSettings settings) |
|
85 | 60 |
{ |
86 | 61 |
_events = events; |
87 | 62 |
_events.Subscribe(this); |
88 | 63 |
|
89 | 64 |
DisplayName = "Pithos Preferences"; |
65 |
Shell = shell; |
|
90 | 66 |
|
91 |
Taskbar=taskbar; |
|
92 |
Taskbar.Parent = this; |
|
93 |
|
|
94 | 67 |
Settings=settings; |
95 | 68 |
|
96 | 69 |
|
97 |
Taskbar.UsageMessage = "Using 15% of 50 GB"; |
|
98 |
Taskbar.StatusMessage = "In Synch"; |
|
99 | 70 |
} |
100 | 71 |
|
72 |
/* |
|
101 | 73 |
protected override void OnViewAttached(object view, object context) |
102 | 74 |
{ |
103 | 75 |
var window = (Window)view; |
... | ... | |
110 | 82 |
{ |
111 | 83 |
var window = (Window)view; |
112 | 84 |
window.Hide(); |
113 |
Taskbar.UpdateStatus(); |
|
114 | 85 |
base.OnViewLoaded(view); |
115 | 86 |
} |
116 | 87 |
|
88 |
*/ |
|
117 | 89 |
#region Preferences Properties |
118 | 90 |
|
119 | 91 |
private bool _noProxy; |
... | ... | |
162 | 134 |
public void SaveChanges() |
163 | 135 |
{ |
164 | 136 |
DoSave(); |
165 |
|
|
137 |
TryClose(true); |
|
166 | 138 |
} |
167 | 139 |
|
168 | 140 |
public void RejectChanges() |
169 | 141 |
{ |
170 | 142 |
Settings.Reload(); |
171 |
|
|
143 |
TryClose(false); |
|
172 | 144 |
} |
173 | 145 |
|
174 | 146 |
public void ApplyChanges() |
... | ... | |
179 | 151 |
private void DoSave() |
180 | 152 |
{ |
181 | 153 |
Settings.Save(); |
182 |
NotifyOfPropertyChange(()=>Settings); |
|
183 |
|
|
184 |
var activeAccount = Settings.Accounts.FirstOrDefault(account => account.IsActive); |
|
185 |
if (activeAccount == null) |
|
186 |
return; |
|
187 |
if (String.IsNullOrWhiteSpace(activeAccount.AccountName)) |
|
188 |
return; |
|
189 |
|
|
190 |
var monitor = Monitors[activeAccount.AccountName]; |
|
191 |
monitor.ApiKey = activeAccount.ApiKey; |
|
192 |
monitor.UserName = activeAccount.AccountName; |
|
193 |
monitor.UsePithos = activeAccount.UsePithos; |
|
154 |
foreach (var account in Settings.Accounts) |
|
155 |
{ |
|
156 |
Shell.MonitorAccount(account); |
|
157 |
} |
|
194 | 158 |
|
195 |
monitor.Start();
|
|
159 |
NotifyOfPropertyChange(()=>Settings);
|
|
196 | 160 |
} |
197 | 161 |
|
198 | 162 |
public void ChangePithosFolder() |
... | ... | |
215 | 179 |
{ |
216 | 180 |
var newAccount = new AccountSettings(); |
217 | 181 |
Settings.Accounts.Add(newAccount); |
218 |
SelectedAccountIndex= Settings.Accounts.Count-1; |
|
182 |
SelectedAccountIndex= Settings.Accounts.Count-1;
|
|
219 | 183 |
NotifyOfPropertyChange(()=>Settings); |
220 | 184 |
} |
221 | 185 |
|
... | ... | |
247 | 211 |
|
248 | 212 |
public void RemoveAccount() |
249 | 213 |
{ |
250 |
Settings.Accounts.RemoveAll(account => account.AccountName == CurrentAccount.AccountName); |
|
251 |
|
|
214 |
var accountName = CurrentAccount.AccountName; |
|
215 |
Settings.Accounts.RemoveAll(account => account.AccountName == accountName); |
|
216 |
|
|
217 |
Shell.RemoveMonitor(accountName); |
|
218 |
|
|
252 | 219 |
NotifyOfPropertyChange(()=>CurrentAccount); |
253 | 220 |
NotifyOfPropertyChange(()=>Settings); |
254 | 221 |
//NotifyOfPropertyChange("Settings.Accounts"); |
... | ... | |
281 | 248 |
} |
282 | 249 |
} |
283 | 250 |
|
284 |
public void RefreshOverlays() |
|
285 |
{ |
|
286 |
string path=Settings.PithosPath; |
|
287 |
if (String.IsNullOrWhiteSpace(path)) |
|
288 |
throw new ArgumentNullException("path", "The path parameter must not be emtpy"); |
|
289 |
|
|
290 |
if (!Directory.Exists(path) && !File.Exists(path)) |
|
291 |
throw new FileNotFoundException("The specified file or path does not exist", path); |
|
292 |
|
|
293 |
|
|
294 |
IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path); |
|
295 |
|
|
296 |
try |
|
297 |
{ |
|
298 |
NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM, |
|
299 |
HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT, |
|
300 |
pathPointer, IntPtr.Zero); |
|
301 |
} |
|
302 |
finally |
|
303 |
{ |
|
304 |
Marshal.FreeHGlobal(pathPointer); |
|
305 |
} |
|
306 |
|
|
307 |
} |
|
251 |
|
|
308 | 252 |
#endregion |
309 | 253 |
|
310 | 254 |
private int _selectedAccountIndex; |
... | ... | |
339 | 283 |
} |
340 | 284 |
|
341 | 285 |
|
342 |
public void Handle(Notification notification) |
|
343 |
{ |
|
344 |
if (!Settings.ShowDesktopNotifications) |
|
345 |
return; |
|
346 |
BalloonIcon icon = BalloonIcon.None; |
|
347 |
switch (notification.Level) |
|
348 |
{ |
|
349 |
case TraceLevel.Error: |
|
350 |
icon = BalloonIcon.Error; |
|
351 |
break; |
|
352 |
case TraceLevel.Info: |
|
353 |
case TraceLevel.Verbose: |
|
354 |
icon = BalloonIcon.Info; |
|
355 |
break; |
|
356 |
case TraceLevel.Warning: |
|
357 |
icon = BalloonIcon.Warning; |
|
358 |
break; |
|
359 |
default: |
|
360 |
icon = BalloonIcon.None; |
|
361 |
break; |
|
362 |
} |
|
363 |
|
|
364 |
var tv = (PreferencesView)this.GetView(); |
|
365 |
tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon); |
|
366 |
} |
|
367 | 286 |
|
368 | 287 |
|
369 | 288 |
public void MoveAccountFolder() |
... | ... | |
381 | 300 |
|
382 | 301 |
var newPath= dlg.SelectedPath; |
383 | 302 |
//Find the account's monitor and stop it |
384 |
var monitor = Monitors[CurrentAccount.AccountName]; |
|
303 |
var monitor = Shell.Monitors[CurrentAccount.AccountName];
|
|
385 | 304 |
monitor.Stop(); |
386 | 305 |
|
387 | 306 |
var oldPath = monitor.RootPath; |
b/trunk/Pithos.Client.WPF/Services/Events.cs | ||
---|---|---|
1 |
namespace Pithos.Client.WPF.Services |
|
2 |
{ |
|
3 |
public class ShowFilePropertiesEvent |
|
4 |
{ |
|
5 |
public string FileName { get; set; } |
|
6 |
|
|
7 |
public ShowFilePropertiesEvent(){} |
|
8 |
|
|
9 |
public ShowFilePropertiesEvent(string fileName) |
|
10 |
{ |
|
11 |
FileName = fileName; |
|
12 |
} |
|
13 |
|
|
14 |
} |
|
15 |
} |
b/trunk/Pithos.Client.WPF/Services/StatusService.cs | ||
---|---|---|
1 |
// ----------------------------------------------------------------------- |
|
2 |
// <copyright file="StatusService.cs" company="Microsoft"> |
|
3 |
// TODO: Update copyright text. |
|
4 |
// </copyright> |
|
5 |
// ----------------------------------------------------------------------- |
|
6 |
|
|
7 |
|
|
8 |
using Caliburn.Micro; |
|
9 |
using System.ServiceModel; |
|
10 |
using System.ComponentModel.Composition; |
|
11 |
using Pithos.Core; |
|
12 |
using Pithos.Interfaces; |
|
13 |
|
|
14 |
namespace Pithos.Client.WPF.Services |
|
15 |
{ |
|
16 |
/// <summary> |
|
17 |
/// TODO: Update summary. |
|
18 |
/// </summary> |
|
19 |
[ServiceBehavior(IncludeExceptionDetailInFaults = true)] |
|
20 |
[Export] |
|
21 |
public class StatusService : IStatusService,ISettingsService, ICommandsService |
|
22 |
{ |
|
23 |
[Import] |
|
24 |
public IPithosSettings Settings { get; set; } |
|
25 |
|
|
26 |
[Import] |
|
27 |
public IStatusChecker Checker { get; set; } |
|
28 |
|
|
29 |
[Import] |
|
30 |
public PithosMonitor Monitor { get; set; } |
|
31 |
|
|
32 |
public StatusService() |
|
33 |
{ |
|
34 |
IoC.BuildUp(this); |
|
35 |
} |
|
36 |
|
|
37 |
private IEventAggregator _events; |
|
38 |
|
|
39 |
[ImportingConstructor] |
|
40 |
public StatusService(IStatusChecker checker,IEventAggregator events) |
|
41 |
{ |
|
42 |
Checker = checker; |
|
43 |
_events = events; |
|
44 |
} |
|
45 |
|
|
46 |
|
|
47 |
public FileOverlayStatus GetStatus(string filePath) |
|
48 |
{ |
|
49 |
return Checker.GetFileOverlayStatus(filePath); |
|
50 |
} |
|
51 |
|
|
52 |
public void DisplayProperties(string filePath) |
|
53 |
{ |
|
54 |
//Monitor. |
|
55 |
} |
|
56 |
|
|
57 |
public PithosSettingsData GetSettings() |
|
58 |
{ |
|
59 |
var data = new PithosSettingsData(Settings); |
|
60 |
return data; |
|
61 |
} |
|
62 |
|
|
63 |
public void ShowProperties(string fileName) |
|
64 |
{ |
|
65 |
_events.Publish(new ShowFilePropertiesEvent(fileName)); |
|
66 |
} |
|
67 |
} |
|
68 |
} |
b/trunk/Pithos.Client.WPF/ShellView.xaml | ||
---|---|---|
5 | 5 |
xmlns:cal="http://www.caliburnproject.org" |
6 | 6 |
> |
7 | 7 |
|
8 |
<Grid Background="White"> |
|
8 |
<Window.Resources> |
|
9 |
<ResourceDictionary> |
|
10 |
<ResourceDictionary.MergedDictionaries> |
|
11 |
<ResourceDictionary Source="PithosStyles.xaml" /> |
|
12 |
</ResourceDictionary.MergedDictionaries> |
|
13 |
</ResourceDictionary> |
|
14 |
</Window.Resources> |
|
15 |
<Grid> |
|
9 | 16 |
<Grid.RowDefinitions> |
10 | 17 |
<RowDefinition Height="*"/> |
11 | 18 |
<RowDefinition Height="Auto"/> |
12 | 19 |
</Grid.RowDefinitions> |
13 |
<tb:TaskbarIcon x:Name="PithosIcon" |
|
14 |
IconSource="/Images/Tray.ico" |
|
15 |
ToolTipText="All Files in Sync" |
|
16 |
MenuActivation="LeftOrRightClick"> |
|
17 |
<tb:TaskbarIcon.ContextMenu> |
|
18 | 20 |
|
19 |
<ContextMenu x:Name="TaskbarMenu" > |
|
20 |
<MenuItem Header="Open PITHOS Folder" x:Name="OpenPithosFolder" cal:Message.Attach="OpenPithosFolder"/> |
|
21 |
<MenuItem Header="Launch PITHOS Site" x:Name="GoToSite" cal:Message.Attach="GoToSite"/> |
|
22 |
<MenuItem Header="Recently Changed Files" > |
|
23 |
<MenuItem x:Name="RecentFiles"/> |
|
21 |
<tb:TaskbarIcon x:Name="TaskbarView" |
|
22 |
IconSource="{Binding StatusIcon}" |
|
23 |
ToolTipText="{Binding StatusMessage}" |
|
24 |
MenuActivation="LeftOrRightClick" |
|
25 |
DoubleClickCommand="{Binding OpenPithosFolderCommand}" |
|
26 |
> |
|
27 |
<tb:TaskbarIcon.ContextMenu> |
|
28 |
<ContextMenu x:Name="TaskbarMenu" > |
|
29 |
<MenuItem Header="Open PITHOS Folder" x:Name="OpenPithosFolder" cal:Message.Attach="OpenPithosFolder" FontWeight="Bold" /> |
|
30 |
<MenuItem Header="Launch PITHOS Site" x:Name="GoToSite" cal:Message.Attach="GoToSite" /> |
|
31 |
<MenuItem Header="Recently Changed Files" x:Name="RecentFiles" ItemsSource="{Binding RecentFiles}"> |
|
32 |
<MenuItem.ItemTemplate> |
|
33 |
<DataTemplate> |
|
34 |
<TextBlock Text="{Binding FileName}"/> |
|
35 |
</DataTemplate> |
|
36 |
</MenuItem.ItemTemplate> |
|
24 | 37 |
</MenuItem> |
25 | 38 |
<Separator /> |
26 |
<MenuItem Header="15% of 50GB used" x:Name="UsageMessage" />
|
|
39 |
<MenuItem Header="{Binding UsageMessage}" x:Name="UsageMessage" />
|
|
27 | 40 |
<Separator /> |
28 |
<MenuItem Header="All Files Up to Date" x:Name="StatusMessage" />
|
|
41 |
<MenuItem Header="{Binding StatusMessage}" x:Name="StatusMessage" />
|
|
29 | 42 |
<Separator /> |
30 |
<MenuItem Header="Pause Synching" x:Name="ToggleSynching" cal:Message.Attach="ToggleSynching"/>
|
|
43 |
<MenuItem Header="{Binding PauseSyncCaption}" x:Name="ToggleSynching" cal:Message.Attach="ToggleSynching" />
|
|
31 | 44 |
<Separator /> |
32 |
<MenuItem Header="Preferences ..." x:Name="ShowPreferences" cal:Message.Attach="ShowPreferences"/> |
|
45 |
<MenuItem Header="Preferences ..." x:Name="ShowPreferences" cal:Message.Attach="ShowPreferences" />
|
|
33 | 46 |
<Separator /> |
34 |
<MenuItem Header="Exit" Name="ExitPithos" cal:Message.Attach="ExitPithos"/> |
|
47 |
<MenuItem Header="Exit" Name="ExitPithos" cal:Message.Attach="ExitPithos" />
|
|
35 | 48 |
</ContextMenu> |
36 | 49 |
</tb:TaskbarIcon.ContextMenu> |
37 | 50 |
</tb:TaskbarIcon> |
38 |
|
|
39 |
|
|
40 |
<TabControl Grid.Row="0"> |
|
41 |
<TabItem Header="General"/> |
|
42 |
<TabItem Header="Accounts"/> |
|
43 |
<TabItem Header="Network"/> |
|
44 |
<TabItem Header="Advanced"/> |
|
45 |
</TabControl> |
|
46 |
|
|
47 |
<StackPanel Orientation="Horizontal" Grid.Row="1"> |
|
48 |
<Button Name="SaveChanges" Content="OK" /> |
|
49 |
<Button Name="RejectChanges" Content="Cancel"/> |
|
50 |
</StackPanel> |
|
51 | 51 |
</Grid> |
52 |
|
|
53 | 52 |
</Window> |
b/trunk/Pithos.Client.WPF/ShellViewModel.cs | ||
---|---|---|
1 |
using System.Windows; |
|
2 |
using System.Windows.Controls; |
|
1 |
using System.Collections.Concurrent; |
|
2 |
using System.ComponentModel.Composition; |
|
3 |
using System.Diagnostics; |
|
4 |
using System.IO; |
|
5 |
using System.Runtime.InteropServices; |
|
6 |
using System.ServiceModel; |
|
7 |
using System.ServiceModel.Description; |
|
8 |
using System.Threading.Tasks; |
|
9 |
using System.Windows; |
|
3 | 10 |
using Caliburn.Micro; |
11 |
using Hardcodet.Wpf.TaskbarNotification; |
|
12 |
using Pithos.Client.WPF.Configuration; |
|
13 |
using Pithos.Client.WPF.Properties; |
|
14 |
using Pithos.Core; |
|
15 |
using Pithos.Interfaces; |
|
16 |
using System; |
|
17 |
using System.Collections.Generic; |
|
18 |
using System.Linq; |
|
19 |
using System.Text; |
|
20 |
using StatusService = Pithos.Client.WPF.Services.StatusService; |
|
4 | 21 |
|
5 | 22 |
namespace Pithos.Client.WPF { |
6 | 23 |
using System.ComponentModel.Composition; |
7 | 24 |
|
8 | 25 |
[Export(typeof(IShell))] |
9 |
public class ShellViewModel : ViewAware, IShell
|
|
26 |
public class ShellViewModel : ViewAware, IStatusNotification, IShell, IHandle<Notification>
|
|
10 | 27 |
{ |
11 | 28 |
|
12 |
|
|
13 |
protected override void OnViewAttached(object view, object context) |
|
29 |
private IStatusChecker _statusChecker; |
|
30 |
private IEventAggregator _events; |
|
31 |
|
|
32 |
public PithosSettings Settings { get; private set; } |
|
33 |
|
|
34 |
public IScreen Parent { get; set; } |
|
35 |
|
|
36 |
|
|
37 |
private Dictionary<string, PithosMonitor> _monitors = new Dictionary<string, PithosMonitor>(); |
|
38 |
public Dictionary<string, PithosMonitor> Monitors |
|
14 | 39 |
{ |
15 |
var window = (Window)view; |
|
16 |
window.WindowStartupLocation = WindowStartupLocation.CenterScreen; |
|
17 |
window.ShowInTaskbar = false; |
|
40 |
get { return _monitors; } |
|
41 |
} |
|
42 |
|
|
43 |
private ServiceHost _statusService { get; set; } |
|
44 |
|
|
45 |
private static readonly log4net.ILog Log = log4net.LogManager.GetLogger("Pithos"); |
|
46 |
|
|
47 |
[ImportingConstructor] |
|
48 |
public ShellViewModel(IWindowManager windowManager, IEventAggregator events, IStatusChecker statusChecker, PithosSettings settings) |
|
49 |
{ |
|
50 |
_windowManager = windowManager; |
|
51 |
OpenPithosFolderCommand = new PithosCommand(OpenPithosFolder); |
|
52 |
_statusChecker = statusChecker; |
|
53 |
_events = events; |
|
54 |
Settings = settings; |
|
18 | 55 |
|
19 |
base.OnViewAttached(view, context); |
|
56 |
|
|
57 |
|
|
58 |
UsageMessage = "Using 15% of 50 GB"; |
|
59 |
StatusMessage = "In Synch"; |
|
60 |
|
|
61 |
foreach (var account in settings.Accounts) |
|
62 |
{ |
|
63 |
|
|
64 |
MonitorAccount(account); |
|
65 |
} |
|
66 |
|
|
67 |
StartStatusService(); |
|
68 |
} |
|
69 |
|
|
70 |
|
|
71 |
|
|
72 |
public void MonitorAccount(AccountSettings account) |
|
73 |
{ |
|
74 |
PithosMonitor monitor = null; |
|
75 |
var accountName = account.AccountName; |
|
76 |
|
|
77 |
if (_monitors.TryGetValue(accountName,out monitor)) |
|
78 |
{ |
|
79 |
//If the account is active |
|
80 |
if (account.IsActive) |
|
81 |
//Start the monitor. It's OK to start an already started monitor, |
|
82 |
//it will just ignore the call |
|
83 |
monitor.Start(); |
|
84 |
else |
|
85 |
{ |
|
86 |
//If the account is inactive |
|
87 |
//Stop and remove the monitor |
|
88 |
RemoveMonitor(accountName); |
|
89 |
} |
|
90 |
return; |
|
91 |
} |
|
92 |
|
|
93 |
//PithosMonitor uses MEF so we need to resolve it |
|
94 |
monitor = new PithosMonitor |
|
95 |
{ |
|
96 |
UserName = accountName, |
|
97 |
ApiKey = account.ApiKey, |
|
98 |
UsePithos = account.UsePithos, |
|
99 |
StatusNotification = this, |
|
100 |
RootPath=account.RootPath |
|
101 |
}; |
|
102 |
IoC.BuildUp(monitor); |
|
103 |
|
|
104 |
var appSettings = Properties.Settings.Default; |
|
105 |
monitor.AuthenticationUrl = account.UsePithos |
|
106 |
? appSettings.PithosAuthenticationUrl |
|
107 |
: appSettings.CloudfilesAuthenticationUrl; |
|
108 |
|
|
109 |
_monitors[accountName] = monitor; |
|
110 |
|
|
111 |
if (account.IsActive) |
|
112 |
{ |
|
113 |
//Don't start a monitor if it doesn't have an account and ApiKey |
|
114 |
if (String.IsNullOrWhiteSpace(monitor.UserName) || String.IsNullOrWhiteSpace(monitor.ApiKey)) |
|
115 |
return; |
|
116 |
StartMonitor(monitor); |
|
117 |
} |
|
20 | 118 |
} |
21 | 119 |
|
22 |
|
|
120 |
|
|
23 | 121 |
protected override void OnViewLoaded(object view) |
24 | 122 |
{ |
25 |
var window = (Window) view;
|
|
123 |
var window = (Window)view; |
|
26 | 124 |
window.Hide(); |
27 |
|
|
125 |
UpdateStatus(); |
|
28 | 126 |
base.OnViewLoaded(view); |
29 | 127 |
} |
30 | 128 |
|
31 | 129 |
|
32 | 130 |
#region Status Properties |
33 |
|
|
131 |
|
|
34 | 132 |
private string _statusMessage; |
35 | 133 |
public string StatusMessage |
36 | 134 |
{ |
... | ... | |
38 | 136 |
set |
39 | 137 |
{ |
40 | 138 |
_statusMessage = value; |
41 |
NotifyOfPropertyChange(()=>StatusMessage);
|
|
139 |
NotifyOfPropertyChange(() => StatusMessage);
|
|
42 | 140 |
} |
43 | 141 |
} |
44 | 142 |
|
... | ... | |
49 | 147 |
set |
50 | 148 |
{ |
51 | 149 |
_usageMessage = value; |
52 |
NotifyOfPropertyChange(()=>UsageMessage);
|
|
150 |
NotifyOfPropertyChange(() => UsageMessage);
|
|
53 | 151 |
} |
54 | 152 |
} |
55 | 153 |
|
56 |
private readonly IObservableCollection<FileEntry> _recentFiles=new BindableCollection<FileEntry>(); |
|
57 |
public IObservableCollection<FileEntry> RecentFiles |
|
154 |
|
|
155 |
private string _pauseSyncCaption="Pause Syncing"; |
|
156 |
public string PauseSyncCaption |
|
157 |
{ |
|
158 |
get { return _pauseSyncCaption; } |
|
159 |
set |
|
160 |
{ |
|
161 |
_pauseSyncCaption = value; |
|
162 |
NotifyOfPropertyChange(() => PauseSyncCaption); |
|
163 |
} |
|
164 |
} |
|
165 |
|
|
166 |
private readonly ObservableConcurrentCollection<FileEntry> _recentFiles = new ObservableConcurrentCollection<FileEntry>(); |
|
167 |
public ObservableConcurrentCollection<FileEntry> RecentFiles |
|
168 |
{ |
|
169 |
get { return _recentFiles; } |
|
170 |
} |
|
171 |
|
|
172 |
|
|
173 |
private string _statusIcon="Images/Tray.ico"; |
|
174 |
public string StatusIcon |
|
58 | 175 |
{ |
59 |
get { return _recentFiles; } |
|
176 |
get { return _statusIcon; } |
|
177 |
set |
|
178 |
{ |
|
179 |
_statusIcon = value; |
|
180 |
NotifyOfPropertyChange(() => StatusIcon); |
|
181 |
} |
|
60 | 182 |
} |
61 | 183 |
|
62 | 184 |
#endregion |
63 | 185 |
|
186 |
#region Commands |
|
64 | 187 |
|
65 | 188 |
public void ShowPreferences() |
66 | 189 |
{ |
67 |
var window = (Window) this.GetView();
|
|
68 |
window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
|
69 |
window.Show();
|
|
190 |
Settings.Reload();
|
|
191 |
var preferences = new PreferencesViewModel(_events, this,Settings);
|
|
192 |
_windowManager.ShowDialog(preferences);
|
|
70 | 193 |
} |
71 | 194 |
|
195 |
|
|
196 |
public PithosCommand OpenPithosFolderCommand { get; private set; } |
|
197 |
|
|
72 | 198 |
public void OpenPithosFolder() |
73 | 199 |
{ |
74 |
|
|
200 |
Process.Start(Settings.PithosPath); |
|
75 | 201 |
} |
76 | 202 |
|
77 | 203 |
public void GoToSite() |
78 | 204 |
{ |
205 |
var activeAccount=Settings.Accounts.FirstOrDefault(account => account.IsActive); |
|
206 |
if (activeAccount == null) |
|
207 |
return; |
|
79 | 208 |
|
209 |
var site = String.Format("http://{0}/ui/?token={1}&user={2}", |
|
210 |
Properties.Settings.Default.PithosSite,activeAccount.ApiKey, |
|
211 |
activeAccount.AccountName); |
|
212 |
Process.Start(site); |
|
80 | 213 |
} |
81 | 214 |
|
215 |
|
|
82 | 216 |
public void ToggleSynching() |
83 | 217 |
{ |
218 |
bool isPaused=false; |
|
219 |
foreach (var pair in Monitors) |
|
220 |
{ |
|
221 |
var monitor = pair.Value; |
|
222 |
monitor.Pause = !monitor.Pause; |
|
223 |
isPaused = monitor.Pause; |
|
224 |
} |
|
225 |
|
|
226 |
PauseSyncCaption = isPaused ? "Resume syncing" : "Pause syncing"; |
|
227 |
var iconKey = isPaused? "TraySyncPaused" : "TrayInSynch"; |
|
228 |
StatusIcon = String.Format(@"Images/{0}.ico", iconKey); |
|
229 |
} |
|
230 |
|
|
231 |
public void ExitPithos() |
|
232 |
{ |
|
233 |
foreach (var pair in Monitors) |
|
234 |
{ |
|
235 |
var monitor = pair.Value; |
|
236 |
monitor.Stop(); |
|
237 |
} |
|
238 |
|
|
239 |
((Window)GetView()).Close(); |
|
240 |
} |
|
241 |
#endregion |
|
242 |
|
|
243 |
|
|
244 |
private Dictionary<PithosStatus, StatusInfo> iconNames = new List<StatusInfo> |
|
245 |
{ |
|
246 |
new StatusInfo(PithosStatus.InSynch, "All files up to date", "TrayInSynch"), |
|
247 |
new StatusInfo(PithosStatus.Syncing, "Syncing Files", "TraySynching"), |
|
248 |
new StatusInfo(PithosStatus.SyncPaused, "Sync Paused", "TraySyncPaused") |
|
249 |
}.ToDictionary(s => s.Status); |
|
250 |
|
|
251 |
readonly IWindowManager _windowManager; |
|
252 |
|
|
253 |
|
|
254 |
public void UpdateStatus() |
|
255 |
{ |
|
256 |
var pithosStatus = _statusChecker.GetPithosStatus(); |
|
257 |
|
|
258 |
if (iconNames.ContainsKey(pithosStatus)) |
|
259 |
{ |
|
260 |
var info = iconNames[pithosStatus]; |
|
261 |
StatusIcon = String.Format(@"Images/{0}.ico", info.IconName); |
|
262 |
StatusMessage = String.Format("Pithos 1.0\r\n{0}", info.StatusText); |
|
263 |
} |
|
264 |
|
|
265 |
var tv=this.GetView(); |
|
266 |
_events.Publish(new Notification { Title = "Start", Message = "Start Monitoring", Level = TraceLevel.Info}); |
|
267 |
} |
|
268 |
|
|
269 |
|
|
270 |
|
|
271 |
private Task StartMonitor(PithosMonitor monitor) |
|
272 |
{ |
|
273 |
return Task.Factory.StartNew(() => |
|
274 |
{ |
|
275 |
using (log4net.ThreadContext.Stacks["Monitor"].Push("Start")) |
|
276 |
{ |
|
277 |
try |
|
278 |
{ |
|
279 |
Log.InfoFormat("Start Monitoring {0}", monitor.UserName); |
|
280 |
monitor.Start(); |
|
281 |
} |
|
282 |
catch (Exception exc) |
|
283 |
{ |
|
284 |
var message = |
|
285 |
String.Format("An exception occured. Can't start monitoring\nWill retry in 10 seconds"); |
|
286 |
Task.Factory.StartNewDelayed(10000, () => StartMonitor(monitor)); |
|
287 |
_events.Publish(new Notification {Title = "Error", Message = message, Level = TraceLevel.Error}); |
|
288 |
Log.Error(message, exc); |
|
289 |
} |
|
290 |
} |
|
291 |
}); |
|
292 |
} |
|
293 |
|
|
294 |
|
|
295 |
public void NotifyChange(string status, TraceLevel level=TraceLevel.Info) |
|
296 |
{ |
|
297 |
this.StatusMessage = status; |
|
84 | 298 |
|
299 |
_events.Publish(new Notification { Title = "Pithos", Message = status, Level = level }); |
|
85 | 300 |
} |
86 | 301 |
|
87 |
public void SaveChanges()
|
|
302 |
public void NotifyChangedFile(string filePath)
|
|
88 | 303 |
{ |
89 |
var window = (Window)GetView(); |
|
90 |
window.Hide(); |
|
304 |
var entry = new FileEntry {FullPath=filePath}; |
|
305 |
IProducerConsumerCollection<FileEntry> files=this.RecentFiles; |
|
306 |
FileEntry popped; |
|
307 |
while (files.Count > 5) |
|
308 |
files.TryTake(out popped); |
|
309 |
files.TryAdd(entry); |
|
91 | 310 |
} |
92 | 311 |
|
93 |
public void RejectChanges() |
|
312 |
|
|
313 |
public void Handle(Notification notification) |
|
94 | 314 |
{ |
95 |
var window=(Window)GetView(); |
|
96 |
window.Hide(); |
|
315 |
if (!Settings.ShowDesktopNotifications) |
|
316 |
return; |
|
317 |
BalloonIcon icon = BalloonIcon.None; |
|
318 |
switch (notification.Level) |
|
319 |
{ |
|
320 |
case TraceLevel.Error: |
|
321 |
icon = BalloonIcon.Error; |
|
322 |
break; |
|
323 |
case TraceLevel.Info: |
|
324 |
case TraceLevel.Verbose: |
|
325 |
icon = BalloonIcon.Info; |
|
326 |
break; |
|
327 |
case TraceLevel.Warning: |
|
328 |
icon = BalloonIcon.Warning; |
|
329 |
break; |
|
330 |
default: |
|
331 |
icon = BalloonIcon.None; |
|
332 |
break; |
|
333 |
} |
|
334 |
|
|
335 |
var tv = (ShellView)this.GetView(); |
|
336 |
tv.TaskbarView.ShowBalloonTip(notification.Title, notification.Message, icon); |
|
97 | 337 |
} |
98 | 338 |
|
339 |
public void RemoveMonitor(string accountName) |
|
340 |
{ |
|
341 |
if (String.IsNullOrWhiteSpace(accountName)) |
|
342 |
return; |
|
343 |
|
|
344 |
PithosMonitor monitor; |
|
345 |
if (Monitors.TryGetValue(accountName, out monitor)) |
|
346 |
{ |
|
347 |
Monitors.Remove(accountName); |
|
348 |
monitor.Stop(); |
|
349 |
} |
|
350 |
} |
|
351 |
|
|
352 |
public void RefreshOverlays() |
|
353 |
{ |
|
354 |
foreach (var pair in Monitors) |
|
355 |
{ |
|
356 |
var monitor = pair.Value; |
|
357 |
|
|
358 |
var path = monitor.RootPath; |
|
359 |
|
|
360 |
if (String.IsNullOrWhiteSpace(path)) |
|
361 |
continue; |
|
362 |
|
|
363 |
if (!Directory.Exists(path) && !File.Exists(path)) |
|
364 |
continue; |
|
365 |
|
|
366 |
IntPtr pathPointer = Marshal.StringToCoTaskMemAuto(path); |
|
367 |
|
|
368 |
try |
|
369 |
{ |
|
370 |
NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_UPDATEITEM, |
|
371 |
HChangeNotifyFlags.SHCNF_PATHW | HChangeNotifyFlags.SHCNF_FLUSHNOWAIT, |
|
372 |
pathPointer, IntPtr.Zero); |
|
373 |
} |
|
374 |
finally |
|
375 |
{ |
|
376 |
Marshal.FreeHGlobal(pathPointer); |
|
377 |
} |
|
378 |
} |
|
379 |
} |
|
380 |
|
|
381 |
private void StartStatusService() |
|
382 |
{ |
|
383 |
// Create a ServiceHost for the CalculatorService type and provide the base address. |
|
384 |
var baseAddress = new Uri("net.pipe://localhost/pithos"); |
|
385 |
_statusService = new ServiceHost(typeof(StatusService), baseAddress); |
|
386 |
|
|
387 |
var binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None); |
|
388 |
|
|
389 |
_statusService.AddServiceEndpoint(typeof(IStatusService), binding, "net.pipe://localhost/pithos/statuscache"); |
|
390 |
_statusService.AddServiceEndpoint(typeof(ISettingsService), binding, "net.pipe://localhost/pithos/settings"); |
|
391 |
|
|
392 |
|
|
393 |
//// Add a mex endpoint |
|
394 |
var smb = new ServiceMetadataBehavior |
|
395 |
{ |
|
396 |
HttpGetEnabled = true, |
|
397 |
HttpGetUrl = new Uri("http://localhost:30000/pithos/mex") |
|
398 |
}; |
|
399 |
_statusService.Description.Behaviors.Add(smb); |
|
400 |
|
|
401 |
|
|
402 |
_statusService.Open(); |
|
403 |
} |
|
404 |
|
|
405 |
private void StopStatusService() |
|
406 |
{ |
|
407 |
if (_statusService == null) |
|
408 |
return; |
|
409 |
|
|
410 |
if (_statusService.State == CommunicationState.Faulted) |
|
411 |
_statusService.Abort(); |
|
412 |
else if (_statusService.State != CommunicationState.Closed) |
|
413 |
_statusService.Close(); |
|
414 |
_statusService = null; |
|
415 |
|
|
416 |
} |
|
99 | 417 |
} |
100 | 418 |
} |
b/trunk/Pithos.Core.Test/NetworkAgentTest.cs | ||
---|---|---|
3 | 3 |
using System.IO; |
4 | 4 |
using System.Linq; |
5 | 5 |
using System.Text; |
6 |
using System.Threading.Tasks; |
|
6 | 7 |
using NUnit.Framework; |
7 | 8 |
using Pithos.Core.Agents; |
8 | 9 |
using Pithos.Network; |
... | ... | |
15 | 16 |
[Test] |
16 | 17 |
public void TestUpload() |
17 | 18 |
{ |
18 |
var agent = new NetworkAgent |
|
19 |
{ |
|
20 |
PithosContainer = "pithos", |
|
21 |
BlockSize = 4194304, |
|
22 |
BlockHash="sha256", |
|
23 |
CloudClient = new CloudFilesClient |
|
24 |
{ |
|
25 |
AuthenticationUrl = @"https://pithos.dev.grnet.gr", |
|
26 |
UsePithos = true |
|
27 |
} |
|
28 |
}; |
|
19 |
var agent = new NetworkAgent(); |
|
29 | 20 |
|
30 | 21 |
var account = "890329@vho.grnet.gr"; |
31 |
agent.CloudClient.Authenticate(account, "24989dce4e0fcb072f8cb60c8922be19"); |
|
22 |
var apiKey = "24989dce4e0fcb072f8cb60c8922be19"; |
|
23 |
|
|
24 |
var client = new CloudFilesClient(account,apiKey) |
|
25 |
{ |
|
26 |
AuthenticationUrl = @"https://pithos.dev.grnet.gr", |
|
27 |
UsePithos = true |
|
28 |
}; |
|
29 |
|
|
30 |
|
|
31 |
var accountInfo=client.Authenticate(); |
|
32 | 32 |
|
33 | 33 |
var fileName = "012345.dump"; |
34 | 34 |
var filePath = Path.Combine(@"e:\pithos\", fileName); |
... | ... | |
47 | 47 |
} |
48 | 48 |
|
49 | 49 |
|
50 |
agent.CloudClient.DeleteObject(null, agent.PithosContainer, fileName,agent.TrashContainer);
|
|
50 |
client.DeleteObject(null, FolderConstants.PithosContainer, fileName);
|
|
51 | 51 |
|
52 |
var task = Signature.CalculateTreeHashAsync(filePath, agent.BlockSize, agent.BlockHash); |
|
53 |
agent.UploadWithHashMap(account,"pithos",new FileInfo(filePath),fileName,task); |
|
52 |
var task = Signature.CalculateTreeHashAsync(filePath, accountInfo.BlockSize, accountInfo.BlockHash); |
|
53 |
var tasks=agent.UploadWithHashMap(accountInfo,account,"pithos",new FileInfo(filePath),fileName,task); |
|
54 |
Task.Factory.Iterate(tasks).Wait(); |
|
54 | 55 |
|
55 |
var newHash = agent.CloudClient.GetHashMap(null, agent.PithosContainer, fileName).Result;
|
|
56 |
var newHash = client.GetHashMap(null, FolderConstants.PithosContainer, fileName).Result;
|
|
56 | 57 |
|
57 | 58 |
|
58 | 59 |
var treeHash = task.Result; |
... | ... | |
67 | 68 |
{ |
68 | 69 |
var fileAgent = new FileAgent {FragmentsPath = @"e:\pithos\Fragments"}; |
69 | 70 |
|
70 |
var agent = new NetworkAgent |
|
71 |
var agent = new NetworkAgent(); |
|
72 |
|
|
73 |
var account = "890329@vho.grnet.gr"; |
|
74 |
var apiKey = "24989dce4e0fcb072f8cb60c8922be19"; |
|
75 |
var client = new CloudFilesClient(account,apiKey) |
|
71 | 76 |
{ |
72 |
PithosContainer = "pithos", |
|
73 |
BlockSize = 4194304, |
|
74 |
BlockHash="sha256", |
|
75 |
CloudClient = new CloudFilesClient |
|
76 |
{ |
|
77 |
AuthenticationUrl = @"https://pithos.dev.grnet.gr", |
|
78 |
UsePithos = true |
|
79 |
}, |
|
80 |
FileAgent=fileAgent |
|
77 |
AuthenticationUrl = @"https://pithos.dev.grnet.gr", |
|
78 |
UsePithos = true |
|
81 | 79 |
}; |
82 | 80 |
|
83 |
var account = "890329@vho.grnet.gr"; |
|
84 |
agent.CloudClient.Authenticate(account, "24989dce4e0fcb072f8cb60c8922be19"); |
|
81 |
var accountInfo=client.Authenticate(); |
|
85 | 82 |
|
86 | 83 |
var fileName = @"AccessDatabaseEngine_x64.exe"; |
87 | 84 |
|
88 | 85 |
var filePath = Path.Combine(@"e:\pithos\", fileName); |
89 | 86 |
if (File.Exists(filePath)) |
90 |
File.Delete(filePath);
|
|
91 |
|
|
92 |
var newHash = agent.CloudClient.GetHashMap(null, agent.PithosContainer, fileName).Result;
|
|
93 |
agent.DownloadWithBlocks(account,agent.PithosContainer,new Uri(fileName,UriKind.Relative),filePath,newHash)
|
|
87 |
File.Delete(filePath); |
|
88 |
|
|
89 |
var newHash = client.GetHashMap(null, FolderConstants.PithosContainer, fileName).Result;
|
|
90 |
agent.DownloadWithBlocks(client, account, FolderConstants.PithosContainer, new Uri(fileName, UriKind.Relative), filePath, newHash)
|
|
94 | 91 |
.Wait(); |
95 | 92 |
|
96 | 93 |
Assert.IsTrue(File.Exists(filePath)); |
97 |
var treeHash = Signature.CalculateTreeHashAsync(filePath, agent.BlockSize, agent.BlockHash).Result;
|
|
94 |
var treeHash = Signature.CalculateTreeHashAsync(filePath, accountInfo.BlockSize, accountInfo.BlockHash).Result;
|
|
98 | 95 |
|
99 | 96 |
Assert.AreEqual(treeHash.TopHash, newHash.TopHash); |
100 | 97 |
|
b/trunk/Pithos.Core.Test/Pithos.Core.Test.csproj | ||
---|---|---|
122 | 122 |
<Project>{A9AE40FF-1A21-414A-9FE7-3BE13644CC6D}</Project> |
123 | 123 |
<Name>Newtonsoft.Json</Name> |
124 | 124 |
</ProjectReference> |
125 |
<ProjectReference Include="..\Libraries\ParallelExtensionsExtras\ParallelExtensionsExtras.csproj"> |
|
126 |
<Project>{C45218F8-09E7-4F57-85BC-5D8D2AC736A3}</Project> |
|
127 |
<Name>ParallelExtensionsExtras</Name> |
|
128 |
</ProjectReference> |
|
125 | 129 |
<ProjectReference Include="..\Pithos.Core\Pithos.Core.csproj"> |
126 | 130 |
<Project>{142AF135-DF30-4563-B0AC-B604235AE874}</Project> |
127 | 131 |
<Name>Pithos.Core</Name> |
b/trunk/Pithos.Core/Agents/CloudTransferAction.cs | ||
---|---|---|
1 | 1 |
using System; |
2 |
using System.Diagnostics.Contracts; |
|
2 | 3 |
using System.IO; |
3 | 4 |
using System.Threading; |
4 | 5 |
using Pithos.Interfaces; |
... | ... | |
18 | 19 |
|
19 | 20 |
public class CloudAction |
20 | 21 |
{ |
22 |
public AccountInfo AccountInfo { get; set; } |
|
21 | 23 |
public CloudActionType Action { get; set; } |
22 | 24 |
public FileInfo LocalFile { get; set; } |
23 | 25 |
public ObjectInfo CloudFile { get; set; } |
... | ... | |
34 | 36 |
} |
35 | 37 |
|
36 | 38 |
|
37 |
protected CloudAction(CloudActionType action) |
|
39 |
[ContractInvariantMethod] |
|
40 |
private void Invariants() |
|
38 | 41 |
{ |
42 |
Contract.Invariant(AccountInfo!=null); |
|
43 |
} |
|
44 |
|
|
45 |
protected CloudAction(AccountInfo accountInfo,CloudActionType action) |
|
46 |
{ |
|
47 |
if (accountInfo==null) |
|
48 |
throw new ArgumentNullException("accountInfo"); |
|
49 |
Contract.EndContractBlock(); |
|
50 |
|
|
39 | 51 |
Action = action; |
52 |
AccountInfo = accountInfo; |
|
40 | 53 |
} |
41 | 54 |
|
42 |
public CloudAction(CloudActionType action, FileInfo localFile, ObjectInfo cloudFile,FileState state,int blockSize, string algorithm) : this(action) |
|
55 |
public CloudAction(AccountInfo accountInfo, CloudActionType action, FileInfo localFile, ObjectInfo cloudFile, FileState state, int blockSize, string algorithm) |
|
56 |
: this(accountInfo,action) |
|
43 | 57 |
{ |
44 | 58 |
LocalFile = localFile; |
45 | 59 |
CloudFile = cloudFile; |
... | ... | |
57 | 71 |
|
58 | 72 |
public class CloudDownloadAction:CloudAction |
59 | 73 |
{ |
60 |
public CloudDownloadAction(ObjectInfo cloudFile) |
|
61 |
:base(CloudActionType.DownloadUnconditional) |
|
74 |
public CloudDownloadAction(AccountInfo accountInfo, ObjectInfo cloudFile)
|
|
75 |
:base(accountInfo,CloudActionType.DownloadUnconditional)
|
|
62 | 76 |
{ |
63 | 77 |
CloudFile = cloudFile; |
64 | 78 |
} |
... | ... | |
66 | 80 |
} |
67 | 81 |
public class CloudDeleteAction:CloudAction |
68 | 82 |
{ |
69 |
public CloudDeleteAction(string fileName, FileState fileState) |
|
70 |
: this(new ObjectInfo { Name = fileName },fileState) |
|
83 |
public CloudDeleteAction(AccountInfo accountInfo, string fileName, FileState fileState)
|
|
84 |
: this(accountInfo,new ObjectInfo { Name = fileName },fileState)
|
|
71 | 85 |
{ |
72 | 86 |
} |
73 | 87 |
|
74 |
public CloudDeleteAction(ObjectInfo cloudFile, FileState fileState) |
|
75 |
: base(CloudActionType.DeleteCloud) |
|
88 |
public CloudDeleteAction(AccountInfo accountInfo, ObjectInfo cloudFile, FileState fileState)
|
|
89 |
: base(accountInfo,CloudActionType.DeleteCloud)
|
|
76 | 90 |
{ |
77 | 91 |
CloudFile = cloudFile; |
78 | 92 |
FileState = fileState; |
... | ... | |
81 | 95 |
|
82 | 96 |
public class CloudUploadAction:CloudAction |
83 | 97 |
{ |
84 |
public CloudUploadAction(FileInfo fileInfo, FileState state, int blockSize, string algorithm) |
|
85 |
: base(CloudActionType.UploadUnconditional,fileInfo,ObjectInfo.Empty,state,blockSize,algorithm) |
|
98 |
public CloudUploadAction(AccountInfo accountInfo, FileInfo fileInfo, FileState state, int blockSize, string algorithm)
|
|
99 |
: base(accountInfo,CloudActionType.UploadUnconditional,fileInfo,ObjectInfo.Empty,state,blockSize,algorithm)
|
|
86 | 100 |
{ |
87 | 101 |
} |
88 | 102 |
} |
... | ... | |
94 | 108 |
public string NewFileName { get; set; } |
95 | 109 |
public string NewPath { get; set; } |
96 | 110 |
|
97 |
public CloudMoveAction(CloudActionType action, string oldPath, string oldFileName, string newFileName, string newPath) |
|
98 |
:base(action) |
|
111 |
public CloudMoveAction(AccountInfo accountInfo, CloudActionType action, string oldPath, string oldFileName, string newFileName, string newPath)
|
|
112 |
:base(accountInfo,action)
|
|
99 | 113 |
{ |
100 | 114 |
OldFileName = oldFileName; |
101 | 115 |
OldPath = oldPath; |
b/trunk/Pithos.Core/Agents/FileAgent.cs | ||
---|---|---|
14 | 14 |
|
15 | 15 |
namespace Pithos.Core.Agents |
16 | 16 |
{ |
17 |
[Export] |
|
17 |
[Export,PartCreationPolicy(CreationPolicy.NonShared)]
|
|
18 | 18 |
public class FileAgent |
19 | 19 |
{ |
20 | 20 |
Agent<WorkflowState> _agent; |
... | ... | |
27 | 27 |
[Import] |
28 | 28 |
public WorkflowAgent WorkflowAgent { get; set; } |
29 | 29 |
|
30 |
public string RootPath { get; private set; } |
|
30 |
private AccountInfo AccountInfo { get; set; } |
|
31 |
|
|
32 |
private string RootPath { get; set; } |
|
31 | 33 |
|
32 | 34 |
private static readonly ILog Log = LogManager.GetLogger("FileAgent"); |
33 | 35 |
|
34 |
public void Start(string rootPath) |
|
36 |
public void Start(AccountInfo accountInfo,string rootPath)
|
|
35 | 37 |
{ |
38 |
if (accountInfo==null) |
|
39 |
throw new ArgumentNullException("accountInfo"); |
|
36 | 40 |
if (String.IsNullOrWhiteSpace(rootPath)) |
37 | 41 |
throw new ArgumentNullException("rootPath"); |
38 | 42 |
if (!Path.IsPathRooted(rootPath)) |
39 | 43 |
throw new ArgumentException("rootPath must be an absolute path","rootPath"); |
40 | 44 |
Contract.EndContractBlock(); |
41 | 45 |
|
46 |
AccountInfo = accountInfo; |
|
42 | 47 |
RootPath = rootPath; |
43 | 48 |
_watcher = new FileSystemWatcher(rootPath); |
44 | 49 |
_watcher.IncludeSubdirectories = true; |
... | ... | |
201 | 206 |
var filePath = e.FullPath; |
202 | 207 |
if (Ignore(filePath)) |
203 | 208 |
return; |
204 |
_agent.Post(new WorkflowState { Path = filePath, FileName = e.Name, TriggeringChange = e.ChangeType }); |
|
209 |
_agent.Post(new WorkflowState(AccountInfo) { Path = filePath, FileName = e.Name, TriggeringChange = e.ChangeType });
|
|
205 | 210 |
} |
206 | 211 |
|
207 | 212 |
|
... | ... | |
213 | 218 |
if (Ignore(oldFullPath) || Ignore(fullPath)) |
214 | 219 |
return; |
215 | 220 |
|
216 |
_agent.Post(new WorkflowState |
|
221 |
_agent.Post(new WorkflowState(AccountInfo)
|
|
217 | 222 |
{ |
218 | 223 |
OldPath = oldFullPath, |
219 | 224 |
OldFileName = e.OldName, |
b/trunk/Pithos.Core/Agents/NetworkAgent.cs | ||
---|---|---|
25 | 25 |
|
26 | 26 |
public IStatusNotification StatusNotification { get; set; } |
27 | 27 |
[Import] |
28 |
public ICloudClient CloudClient { get; set; } |
|
29 |
|
|
30 |
[Import] |
|
31 | 28 |
public FileAgent FileAgent {get;set;} |
32 | 29 |
|
33 |
/* |
|
34 |
[Import] |
|
35 |
public IPithosWorkflow Workflow { get; set; } |
|
36 |
*/ |
|
37 |
|
|
38 |
|
|
39 |
public string PithosContainer { get; set; } |
|
40 |
public string TrashContainer { get; private set; } |
|
41 |
public IList<string> Containers { get; private set; } |
|
42 |
|
|
43 |
public int BlockSize { get; set; } |
|
44 |
public string BlockHash { get; set; } |
|
30 |
/* public int BlockSize { get; set; } |
|
31 |
public string BlockHash { get; set; }*/ |
|
45 | 32 |
|
46 | 33 |
private static readonly ILog Log = LogManager.GetLogger("NetworkAgent"); |
47 | 34 |
|
35 |
private List<AccountInfo> _accounts=new List<AccountInfo>(); |
|
48 | 36 |
|
49 |
public void Start(string pithosContainer, string trashContainer, int blockSize, string blockHash)
|
|
37 |
public void Start(/*int blockSize, string blockHash*/)
|
|
50 | 38 |
{ |
51 |
if (String.IsNullOrWhiteSpace(pithosContainer)) |
|
52 |
throw new ArgumentNullException("pithosContainer"); |
|
53 |
if (String.IsNullOrWhiteSpace(trashContainer)) |
|
54 |
throw new ArgumentNullException("trashContainer"); |
|
39 |
/* |
|
40 |
if (blockSize<0) |
|
41 |
throw new ArgumentOutOfRangeException("blockSize"); |
|
42 |
if (String.IsNullOrWhiteSpace(blockHash)) |
|
43 |
throw new ArgumentOutOfRangeException("blockHash"); |
|
55 | 44 |
Contract.EndContractBlock(); |
45 |
*/ |
|
56 | 46 |
|
57 |
PithosContainer = pithosContainer; |
|
58 |
TrashContainer = trashContainer; |
|
47 |
/* |
|
59 | 48 |
BlockSize = blockSize; |
60 | 49 |
BlockHash = blockHash; |
50 |
*/ |
|
61 | 51 |
|
62 | 52 |
|
63 | 53 |
_agent = Agent<CloudAction>.Start(inbox => |
... | ... | |
77 | 67 |
{ |
78 | 68 |
if (action == null) |
79 | 69 |
throw new ArgumentNullException("action"); |
70 |
if (action.AccountInfo==null) |
|
71 |
throw new ArgumentException("The action.AccountInfo is empty","action"); |
|
80 | 72 |
Contract.EndContractBlock(); |
81 | 73 |
|
74 |
var accountInfo = action.AccountInfo; |
|
75 |
|
|
82 | 76 |
using (log4net.ThreadContext.Stacks["NETWORK"].Push("PROCESS")) |
83 | 77 |
{ |
84 | 78 |
Log.InfoFormat("[ACTION] Start Processing {0}:{1}->{2}", action.Action, action.LocalFile, |
85 | 79 |
action.CloudFile.Name); |
86 | 80 |
|
87 | 81 |
var localFile = action.LocalFile; |
88 |
var cloudFile = action.CloudFile; |
|
82 |
var cloudFile = action.CloudFile;
|
|
89 | 83 |
var downloadPath = (cloudFile == null) |
90 | 84 |
? String.Empty |
91 |
: Path.Combine(FileAgent.RootPath, cloudFile.RelativeUrlToFilePath(CloudClient.UserName));
|
|
85 |
: Path.Combine(accountInfo.AccountPath, cloudFile.RelativeUrlToFilePath(accountInfo.UserName));
|
|
92 | 86 |
|
93 | 87 |
try |
94 | 88 |
{ |
95 |
var account = action.CloudFile.Account ?? CloudClient.UserName;
|
|
96 |
var container = action.CloudFile.Container ?? PithosContainer; |
|
89 |
var account = action.CloudFile.Account ?? accountInfo.UserName;
|
|
90 |
var container = action.CloudFile.Container ?? FolderConstants.PithosContainer;
|
|
97 | 91 |
|
98 | 92 |
switch (action.Action) |
99 | 93 |
{ |
100 | 94 |
case CloudActionType.UploadUnconditional: |
101 |
UploadCloudFile(account, container, localFile, action.LocalHash.Value, action.TopHash.Value); |
|
95 |
UploadCloudFile(accountInfo,account, container, localFile, action.LocalHash.Value, action.TopHash.Value);
|
|
102 | 96 |
break; |
103 | 97 |
case CloudActionType.DownloadUnconditional: |
104 | 98 |
|
105 |
DownloadCloudFile(account, container, new Uri(cloudFile.Name, UriKind.Relative), |
|
99 |
DownloadCloudFile(accountInfo, account, container, new Uri(cloudFile.Name, UriKind.Relative),
|
|
106 | 100 |
downloadPath); |
107 | 101 |
break; |
108 | 102 |
case CloudActionType.DeleteCloud: |
109 |
DeleteCloudFile(account, container, cloudFile.Name); |
|
103 |
DeleteCloudFile(accountInfo, account, container, cloudFile.Name);
|
|
110 | 104 |
break; |
111 | 105 |
case CloudActionType.RenameCloud: |
112 | 106 |
var moveAction = (CloudMoveAction)action; |
113 |
RenameCloudFile(account, container, moveAction.OldFileName, moveAction.NewPath, |
|
107 |
RenameCloudFile(accountInfo, account, container, moveAction.OldFileName, moveAction.NewPath,
|
|
114 | 108 |
moveAction.NewFileName); |
115 | 109 |
break; |
116 | 110 |
case CloudActionType.MustSynch: |
... | ... | |
118 | 112 |
if (!File.Exists(downloadPath)) |
119 | 113 |
{ |
120 | 114 |
var cloudUri = new Uri(action.CloudFile.Name, UriKind.Relative); |
121 |
DownloadCloudFile(account, container, cloudUri, downloadPath); |
|
115 |
DownloadCloudFile(accountInfo, account, container, cloudUri, downloadPath);
|
|
122 | 116 |
} |
123 | 117 |
else |
124 | 118 |
{ |
125 |
SyncFiles(action); |
|
119 |
SyncFiles(accountInfo, action);
|
|
126 | 120 |
} |
127 | 121 |
break; |
128 | 122 |
} |
... | ... | |
137 | 131 |
{ |
138 | 132 |
Log.ErrorFormat("{0} : {1} -> {2} failed because the file was not found.\n Rescheduling a delete", |
139 | 133 |
action.Action, action.LocalFile, action.CloudFile, exc); |
140 |
Post(new CloudDeleteAction(action.CloudFile,action.FileState)); |
|
134 |
Post(new CloudDeleteAction(accountInfo,action.CloudFile,action.FileState));
|
|
141 | 135 |
} |
142 | 136 |
catch (Exception exc) |
143 | 137 |
{ |
... | ... | |
150 | 144 |
} |
151 | 145 |
} |
152 | 146 |
|
153 |
private void SyncFiles(CloudAction action) |
|
147 |
private void SyncFiles(AccountInfo accountInfo,CloudAction action)
|
|
154 | 148 |
{ |
149 |
if (accountInfo == null) |
|
150 |
throw new ArgumentNullException("accountInfo"); |
|
155 | 151 |
if (action==null) |
156 | 152 |
throw new ArgumentNullException("action"); |
157 | 153 |
if (action.LocalFile==null) |
... | ... | |
168 | 164 |
|
169 | 165 |
var account = cloudFile.Account; |
170 | 166 |
//Use "pithos" by default if no container is specified |
171 |
var container = cloudFile.Container ?? PithosContainer; |
|
167 |
var container = cloudFile.Container ?? FolderConstants.PithosContainer;
|
|
172 | 168 |
|
173 | 169 |
var cloudUri = new Uri(cloudFile.Name, UriKind.Relative); |
174 | 170 |
var cloudHash = cloudFile.Hash.ToLower(); |
... | ... | |
192 | 188 |
if (lastUpTime <= lastLocalTime) |
193 | 189 |
{ |
194 | 190 |
//It probably means it was changed while the app was down |
195 |
UploadCloudFile(account, container, localFile, action.LocalHash.Value, |
|
191 |
UploadCloudFile(accountInfo,account, container, localFile, action.LocalHash.Value,
|
|
196 | 192 |
action.TopHash.Value); |
197 | 193 |
} |
198 | 194 |
else |
... | ... | |
204 | 200 |
{ |
205 | 201 |
case FileStatus.Unchanged: |
206 | 202 |
//If the local file's status is Unchanged, we can go on and download the newer cloud file |
207 |
DownloadCloudFile(account, container,cloudUri,downloadPath); |
|
203 |
DownloadCloudFile(accountInfo,account, container,cloudUri,downloadPath);
|
|
208 | 204 |
break; |
209 | 205 |
case FileStatus.Modified: |
210 | 206 |
//If the local file is Modified, we may have a conflict. In this case we should mark the file as Conflict |
... | ... | |
273 | 269 |
{ |
274 | 270 |
if (cloudAction == null) |
275 | 271 |
throw new ArgumentNullException("cloudAction"); |
272 |
if (cloudAction.AccountInfo==null) |
|
273 |
throw new ArgumentException("The CloudAction.AccountInfo is empty","cloudAction"); |
|
276 | 274 |
Contract.EndContractBlock(); |
277 | 275 |
|
278 | 276 |
//If the action targets a local file, add a treehash calculation |
279 | 277 |
if (cloudAction.LocalFile != null) |
280 | 278 |
{ |
281 |
|
|
282 |
if (cloudAction.LocalFile.Length>BlockSize) |
|
283 |
cloudAction.TopHash = new Lazy<string>(() => Signature.CalculateTreeHashAsync(cloudAction.LocalFile,
|
|
284 |
BlockSize, BlockHash).Result
|
|
279 |
var accountInfo = cloudAction.AccountInfo; |
|
280 |
if (cloudAction.LocalFile.Length>accountInfo.BlockSize)
|
|
281 |
cloudAction.TopHash = new Lazy<string>(() => Signature.CalculateTreeHashAsync(cloudAction.LocalFile, |
|
282 |
accountInfo.BlockSize, accountInfo.BlockHash).Result
|
|
285 | 283 |
.TopHash.ToHashString()); |
286 | 284 |
else |
287 | 285 |
{ |
... | ... | |
305 | 303 |
} |
306 | 304 |
} |
307 | 305 |
|
306 |
|
|
307 |
|
|
308 | 308 |
//Remote files are polled periodically. Any changes are processed |
309 |
public Task ProcessRemoteFiles(string accountPath,DateTime? since=null) |
|
309 |
public Task ProcessRemoteFiles(DateTime? since=null) |
|
310 |
{ |
|
311 |
return Task<Task>.Factory.StartNewDelayed(10000, () => |
|
312 |
{ |
|
313 |
using (log4net.ThreadContext.Stacks["Retrieve Remote"].Push("All accounts")) |
|
314 |
{ |
|
315 |
//Next time we will check for all changes since the current check minus 1 second |
|
316 |
//This is done to ensure there are no discrepancies due to clock differences |
|
317 |
DateTime nextSince = DateTime.Now.AddSeconds(-1); |
|
318 |
|
|
319 |
var tasks=from accountInfo in _accounts |
|
320 |
select ProcessAccountFiles(accountInfo, since); |
|
321 |
var process=Task.Factory.Iterate(tasks); |
|
322 |
|
|
323 |
return process.ContinueWith(t=> |
|
324 |
ProcessRemoteFiles(nextSince)); |
|
325 |
} |
|
326 |
}); |
|
327 |
} |
|
328 |
|
|
329 |
public Task ProcessAccountFiles(AccountInfo accountInfo,DateTime? since=null) |
|
310 | 330 |
{ |
311 |
if (String.IsNullOrWhiteSpace(accountPath)) |
|
312 |
throw new ArgumentNullException(accountPath); |
|
331 |
if (accountInfo==null) |
|
332 |
throw new ArgumentNullException("accountInfo"); |
|
333 |
if (String.IsNullOrWhiteSpace(accountInfo.AccountPath)) |
|
334 |
throw new ArgumentException("The AccountInfo.AccountPath is empty","accountInfo"); |
|
313 | 335 |
Contract.EndContractBlock(); |
314 | 336 |
|
315 |
using (log4net.ThreadContext.Stacks["SCHEDULE"].Push("Retrieve Remote"))
|
|
337 |
using (log4net.ThreadContext.Stacks["Retrieve Remote"].Push(accountInfo.UserName))
|
|
316 | 338 |
{ |
317 |
Log.Info("[LISTENER] Scheduled"); |
|
339 |
Log.Info("Scheduled"); |
|
340 |
var client=new CloudFilesClient(accountInfo); |
|
318 | 341 |
|
319 | 342 |
//Get the list of server objects changed since the last check |
320 |
var listObjects = Task<IList<ObjectInfo>>.Factory.StartNewDelayed(10000, () => |
|
321 |
CloudClient.ListObjects(CloudClient.UserName, PithosContainer, since)); |
|
343 |
var listObjects = Task<IList<ObjectInfo>>.Factory.StartNew(() => |
Also available in: Unified diff