Revision 69588a95
b/trunk/Pithos.Client.WPF.Test/NodeTest.cs | ||
---|---|---|
3 | 3 |
using System.Linq; |
4 | 4 |
using System.Text; |
5 | 5 |
using NUnit.Framework; |
6 |
using Pithos.Client.WPF.SelectiveSynch; |
|
6 | 7 |
using Pithos.Client.WPF.Utils; |
8 |
using Pithos.Interfaces; |
|
7 | 9 |
|
8 | 10 |
namespace Pithos.Client.WPF.Test |
9 | 11 |
{ |
... | ... | |
120 | 122 |
}; |
121 | 123 |
|
122 | 124 |
Assert.That(source.ToTree(s=>s.Item1,s=>s.Item2).First().Equals(target), Is.True); |
125 |
} [Test] |
|
126 |
|
|
127 |
public void TestObjectInfoToTree() |
|
128 |
{ |
|
129 |
var target = new DirectoryRecord{ DisplayName= "Root", |
|
130 |
Directories = |
|
131 |
{ |
|
132 |
new DirectoryRecord{DisplayName = "Root/DisplayName1", |
|
133 |
Directories = |
|
134 |
{ |
|
135 |
new DirectoryRecord{DisplayName="Root/DisplayName1/File2"}, |
|
136 |
new DirectoryRecord{DisplayName="Root/DisplayName1/DisplayName11", |
|
137 |
Directories= |
|
138 |
{ |
|
139 |
new DirectoryRecord{DisplayName="Root/DisplayName1/DisplayName11/DisplayName111", |
|
140 |
Directories= |
|
141 |
{ |
|
142 |
new DirectoryRecord{DisplayName="Root/DisplayName1/DisplayName11/DisplayName111/File1"} |
|
143 |
}} |
|
144 |
} |
|
145 |
}, |
|
146 |
} |
|
147 |
}, |
|
148 |
} |
|
149 |
}; |
|
150 |
var account = "someaccount"; |
|
151 |
var container = "Root"; |
|
152 |
var source= new[] |
|
153 |
{ |
|
154 |
new ObjectInfo{Account=account,Container=container,Name="Path1",Content_Type="application/directory"}, |
|
155 |
new ObjectInfo{Account=account,Container=container,Name="Path1/Path11",Content_Type="application/folder"}, |
|
156 |
new ObjectInfo{Account=account,Container=container,Name="Path1/Path11/Path111"}, |
|
157 |
new ObjectInfo{Account=account,Container=container,Name="Path1/Path11/Path111/File1",Content_Type="application/octet-stream"}, |
|
158 |
new ObjectInfo{Account=account,Container=container,Name="Path1/File2"}, |
|
159 |
new ObjectInfo{Account=account,Container=container,Name="Path2/File2"}, |
|
160 |
new ObjectInfo{Account=account,Container=container,Name="Path2/Path21/File2"}, |
|
161 |
new ObjectInfo{Account=account,Container=container,Name="File02"}, |
|
162 |
new ObjectInfo{Account=account,Container=container,Name="File03"} |
|
163 |
}; |
|
164 |
|
|
165 |
var tree = source.ToTree(); |
|
166 |
var allNodes = (from DirectoryRecord root in tree |
|
167 |
from DirectoryRecord record in root |
|
168 |
select record).ToList(); |
|
169 |
Assert.That(allNodes.Count,Is.EqualTo(5)); |
|
123 | 170 |
} |
124 | 171 |
} |
125 | 172 |
} |
b/trunk/Pithos.Client.WPF.Test/Pithos.Client.WPF.Test.csproj | ||
---|---|---|
61 | 61 |
<Project>{4D9406A3-50ED-4672-BB97-A0B3EA4946FE}</Project> |
62 | 62 |
<Name>Pithos.Client.WPF</Name> |
63 | 63 |
</ProjectReference> |
64 |
<ProjectReference Include="..\Pithos.Interfaces\Pithos.Interfaces.csproj"> |
|
65 |
<Project>{7EEFF32F-CCF8-436A-9E0B-F40434C09AF4}</Project> |
|
66 |
<Name>Pithos.Interfaces</Name> |
|
67 |
</ProjectReference> |
|
64 | 68 |
</ItemGroup> |
65 | 69 |
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> |
66 | 70 |
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. |
b/trunk/Pithos.Client.WPF/FileProperties/ConflictsView.xaml | ||
---|---|---|
3 | 3 |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:cal="http://www.caliburnproject.org" |
4 | 4 |
xmlns:cnv="clr-namespace:Pithos.Client.WPF.Converters" |
5 | 5 |
xmlns:model="clr-namespace:Pithos.Client.WPF.FileProperties" |
6 |
Title="Conflicts" Height="500" Width="500" x:Name="This" |
|
7 |
> |
|
6 |
Title="Conflicts" Height="500" Width="500" x:Name="This" xmlns:my="clr-namespace:Microsoft.Windows.Controls.Core.Converters;assembly=WPFToolkit.Extended" Icon="/PithosPlus;component/Images/Pithos.ico"> |
|
8 | 7 |
<Window.Resources> |
9 | 8 |
<ResourceDictionary> |
10 | 9 |
<ContextMenu x:Key="RowMenu" DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}"> |
... | ... | |
13 | 12 |
<Style x:Key="DefaultRowStyle" TargetType="{x:Type DataGridRow}"> |
14 | 13 |
<Setter Property="ContextMenu" Value="{StaticResource RowMenu}" /> |
15 | 14 |
</Style> |
16 |
<ResourceDictionary.MergedDictionaries> |
|
15 |
<BooleanToVisibilityConverter x:Key="BoolToVisible" /> |
|
16 |
<my:InverseBoolConverter x:Key="BoolToInvisible" /> |
|
17 |
<ResourceDictionary.MergedDictionaries> |
|
17 | 18 |
<ResourceDictionary Source="..\PithosStyles.xaml" /> |
18 | 19 |
</ResourceDictionary.MergedDictionaries> |
19 | 20 |
</ResourceDictionary> |
... | ... | |
24 | 25 |
<RowDefinition Height="Auto"/> |
25 | 26 |
</Grid.RowDefinitions> |
26 | 27 |
|
27 |
<DataGrid x:Name="Conflicts" HorizontalContentAlignment="Stretch" Grid.Row="0" AutoGenerateColumns="false" RowStyle="{StaticResource DefaultRowStyle}"> |
|
28 |
<DataGrid x:Name="Conflicts" HorizontalContentAlignment="Stretch" Grid.Row="0" |
|
29 |
AutoGenerateColumns="false" |
|
30 |
RowStyle="{StaticResource DefaultRowStyle}" |
|
31 |
Visibility="{Binding Converter={StaticResource BoolToVisible}, Path=HasConflicts}" |
|
32 |
> |
|
28 | 33 |
<DataGrid.Columns> |
29 | 34 |
<DataGridTextColumn x:Name="FilePath" Binding="{Binding FilePath}" Header="File" Width="*" /> |
30 | 35 |
<DataGridTextColumn x:Name="LocalModified" Binding="{Binding LocalModified}" Header="Local Date" Width="Auto" /> |
... | ... | |
43 | 48 |
</DataGridTemplateColumn> |
44 | 49 |
</DataGrid.Columns> |
45 | 50 |
</DataGrid> |
51 |
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" TextAlignment="Center" Visibility="{Binding Converter={StaticResource BoolToInvisible}, Path=HasConflicts}" FontWeight="Bold">There are no conflicts.</TextBlock> |
|
46 | 52 |
<StackPanel Orientation="Horizontal" Grid.Row="2" HorizontalAlignment="Right"> |
47 | 53 |
<Button Name="Apply" Content="OK" Margin="5,5,10,5" Style="{StaticResource ButtonStyle}" IsDefault="False" /> |
48 | 54 |
<Button Name="Cancel" Content="Cancel" Margin="5,5,10,5" Style="{StaticResource ButtonStyle}" IsCancel="True" /> |
b/trunk/Pithos.Client.WPF/FileProperties/ConflictsViewModel.cs | ||
---|---|---|
9 | 9 |
using System.Text; |
10 | 10 |
using Caliburn.Micro; |
11 | 11 |
using Pithos.Client.WPF.Converters; |
12 |
using Pithos.Client.WPF.Utils; |
|
12 | 13 |
using Pithos.Core; |
13 | 14 |
using Pithos.Interfaces; |
14 | 15 |
|
... | ... | |
45 | 46 |
private string _reason; |
46 | 47 |
public string Reason |
47 | 48 |
{ |
48 |
get { return _reason; } |
|
49 |
get |
|
50 |
{ |
|
51 |
return _reason; |
|
52 |
} |
|
49 | 53 |
set |
50 | 54 |
{ |
51 | 55 |
_reason = value; |
... | ... | |
91 | 95 |
get { return _conflicts; } |
92 | 96 |
} |
93 | 97 |
|
98 |
public bool HasConflicts |
|
99 |
{ |
|
100 |
get { return Conflicts!=null && Conflicts.Count > 0; } |
|
101 |
} |
|
102 |
|
|
94 | 103 |
public string[] Actions |
95 | 104 |
{ |
96 | 105 |
get { return new[] {"Keep Local", "Keep Server", "Keep Both"}; } |
... | ... | |
105 | 114 |
select state; |
106 | 115 |
var conflicts = from state in fileStates |
107 | 116 |
let info=FileInfoExtensions.FromPath(state.FilePath) |
108 |
select new ConflictFile {FilePath = state.FilePath,Reason=state.ConflictReason,LocalModified = info.LastWriteTime}; |
|
117 |
select new ConflictFile |
|
118 |
{ |
|
119 |
FilePath = state.FilePath, |
|
120 |
Reason=state.ConflictReason??state.FileStatus.Name() , |
|
121 |
LocalModified = info.LastWriteTime |
|
122 |
}; |
|
109 | 123 |
_conflicts = new ObservableCollection<ConflictFile>(conflicts.ToList()); |
110 | 124 |
|
111 | 125 |
} |
b/trunk/Pithos.Client.WPF/Preferences/PreferencesViewModel.cs | ||
---|---|---|
488 | 488 |
|
489 | 489 |
var cachePath = Path.Combine(CurrentAccount.RootPath, FolderConstants.CacheFolder); |
490 | 490 |
var dir = new DirectoryInfo(cachePath); |
491 |
//The file may not exist if we just created the account |
|
492 |
if (!dir.Exists) |
|
493 |
return; |
|
491 | 494 |
dir.EnumerateFiles().Apply(file=>file.Delete()); |
492 | 495 |
dir.EnumerateDirectories().Apply(folder => folder.Delete(true)); |
493 | 496 |
} |
b/trunk/Pithos.Client.WPF/SelectiveSynch/SelectiveSynchViewModel.cs | ||
---|---|---|
109 | 109 |
{ |
110 | 110 |
DisplayName = container.Name, |
111 | 111 |
Uri=new Uri(client.StorageUrl,String.Format(@"{0}/{1}",Account.AccountName, container.Name)), |
112 |
Directories = (from dir in client.ListObjects(AccountName, container.Name) |
|
113 |
where dir.IsDirectory |
|
112 |
Directories = (from dir in client.ListObjects(AccountName, container.Name) |
|
114 | 113 |
select dir).ToTree() |
115 | 114 |
}; |
116 | 115 |
var ownFolders = dirs.ToList(); |
... | ... | |
125 | 124 |
{ |
126 | 125 |
DisplayName=container.Name, |
127 | 126 |
Uri = new Uri(client.StorageUrl, "../" + account.name + "/" + container.Name), |
128 |
Directories=(from folder in client.ListObjects(account.name,container.Name) |
|
129 |
where folder.IsDirectory |
|
127 |
Directories=(from folder in client.ListObjects(account.name,container.Name) |
|
130 | 128 |
select folder).ToTree() |
131 | 129 |
}).ToList() |
132 | 130 |
}; |
b/trunk/Pithos.Client.WPF/Utils/EnumerableExtensions.cs | ||
---|---|---|
4 | 4 |
using System.Linq; |
5 | 5 |
using System.Linq.Expressions; |
6 | 6 |
using System.Text; |
7 |
using System.Text.RegularExpressions; |
|
7 | 8 |
using Pithos.Client.WPF.SelectiveSynch; |
9 |
using Pithos.Core; |
|
8 | 10 |
using Pithos.Interfaces; |
9 | 11 |
|
10 | 12 |
namespace Pithos.Client.WPF.Utils |
... | ... | |
79 | 81 |
|
80 | 82 |
public static List<DirectoryRecord> ToTree(this IEnumerable<ObjectInfo> enumerable) |
81 | 83 |
{ |
84 |
//Order the items to ensure that children always come after their parents |
|
82 | 85 |
var orderedItems=enumerable.OrderBy(o=>o.Uri.ToString()); |
86 |
//Each item is stored in lookups |
|
83 | 87 |
var lookups = new Dictionary<string,DirectoryRecord>(); |
84 |
var nodes = new List<DirectoryRecord>(); |
|
88 |
|
|
89 |
//RootNodes contains only the root nodes |
|
90 |
var rootNodes = new List<DirectoryRecord>(); |
|
91 |
|
|
85 | 92 |
foreach (var item in orderedItems) |
86 | 93 |
{ |
87 | 94 |
var path = item.Uri.ToString(); |
88 |
var newNode = new DirectoryRecord{ DisplayName=item.Name.Split('/').Last(),ObjectInfo=item}; |
|
89 |
lookups[path] = newNode; |
|
90 |
|
|
91 |
var lastIndex = path.LastIndexOf("/", StringComparison.Ordinal); |
|
92 |
var upTo = lastIndex < 0 ? path.Length - 1 : lastIndex; |
|
93 |
var parentPath = path.Substring(0, upTo); |
|
94 |
|
|
95 |
//Calculate the parent path |
|
96 |
var parentPath = GetParentPath(path); |
|
97 |
var parentName = GetParentPath(item.Name); |
|
95 | 98 |
DirectoryRecord parent; |
96 |
if (lookups.TryGetValue(parentPath, out parent)) |
|
99 |
DirectoryRecord newNode; |
|
100 |
|
|
101 |
//Dont't add files |
|
102 |
if (!item.IsDirectory) |
|
97 | 103 |
{ |
98 |
parent.Directories.Add(newNode); |
|
99 |
parent.Directories.Sort((x,y)=>String.CompareOrdinal(x.Uri.ToString(), y.Uri.ToString())); |
|
104 |
//But check to ensure that we DO have it's parent on record |
|
105 |
//It it exist |
|
106 |
if (lookups.TryGetValue(parentPath, out parent)) |
|
107 |
{ |
|
108 |
//Just continue |
|
109 |
continue; |
|
110 |
} |
|
111 |
//If the item is directly below its parent container, there is no path to add |
|
112 |
if (String.IsNullOrWhiteSpace(parentName)) |
|
113 |
continue; |
|
114 |
//Otherwise we need to add it, because it is missing from the list |
|
115 |
//Store each item using its current path |
|
116 |
newNode = new DirectoryRecord { DisplayName = parentPath.Split('/').Last(), |
|
117 |
ObjectInfo = new ObjectInfo{Account=item.Account,Container=item.Container,Name=parentPath,Content_Type="application/directory"}}; |
|
100 | 118 |
} |
101 | 119 |
else |
102 |
nodes.Add(newNode); |
|
120 |
{ |
|
121 |
//Store each item using its current path |
|
122 |
newNode = new DirectoryRecord {DisplayName = item.Name.Split('/').Last(), ObjectInfo = item}; |
|
123 |
} |
|
124 |
AddNode(rootNodes, parentPath, path, lookups, newNode); |
|
125 |
} |
|
126 |
return rootNodes; |
|
127 |
} |
|
128 |
|
|
129 |
private static void AddNode(List<DirectoryRecord> rootNodes, string parentPath, string path, Dictionary<string, DirectoryRecord> lookups, DirectoryRecord newNode) |
|
130 |
{ |
|
131 |
DirectoryRecord parent; |
|
132 |
lookups[path] = newNode; |
|
103 | 133 |
|
134 |
|
|
135 |
//Does a parent item exist? |
|
136 |
if (lookups.TryGetValue(parentPath, out parent)) |
|
137 |
{ |
|
138 |
//If so, add the current item under its parent |
|
139 |
parent.Directories.Add(newNode); |
|
140 |
parent.Directories.Sort((x, y) => String.CompareOrdinal(x.Uri.ToString(), y.Uri.ToString())); |
|
104 | 141 |
} |
105 |
return nodes; |
|
142 |
else |
|
143 |
//Otherwise add it to the list of root nodes |
|
144 |
rootNodes.Add(newNode); |
|
145 |
} |
|
146 |
|
|
147 |
private static string GetParentPath(string path) |
|
148 |
{ |
|
149 |
var lastIndex = path.LastIndexOf("/", StringComparison.Ordinal); |
|
150 |
if (lastIndex < 0) |
|
151 |
return null; |
|
152 |
var parentPath = path.Substring(0, lastIndex); |
|
153 |
return parentPath; |
|
154 |
} |
|
155 |
|
|
156 |
static readonly Regex PascalCaseRegex = new Regex("[a-z][A-Z]", RegexOptions.Compiled); |
|
157 |
public static string Name(this Enum value) |
|
158 |
{ |
|
159 |
var name = Enum.GetName(value.GetType(), value); |
|
160 |
return PascalCaseRegex.Replace(name, m => m.Value[0] + " " + char.ToLower(m.Value[1])); |
|
106 | 161 |
} |
107 | 162 |
} |
108 | 163 |
} |
b/trunk/Pithos.Core/Agents/StatusAgent.cs | ||
---|---|---|
363 | 363 |
} |
364 | 364 |
} |
365 | 365 |
|
366 |
private int UpdateStatusDirect(string absolutePath, FileStatus fileStatus, FileOverlayStatus overlayStatus) |
|
366 |
private int UpdateStatusDirect(string absolutePath, FileStatus fileStatus, FileOverlayStatus overlayStatus, string conflictReason)
|
|
367 | 367 |
{ |
368 | 368 |
using (log4net.ThreadContext.Stacks["StatusAgent"].Push("UpdateStatusDirect")) |
369 | 369 |
{ |
... | ... | |
376 | 376 |
using ( |
377 | 377 |
var command = |
378 | 378 |
new SQLiteCommand( |
379 |
"update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus where FilePath = :path COLLATE NOCASE ", |
|
379 |
"update FileState set OverlayStatus= :overlayStatus, FileStatus= :fileStatus,ConflictReason= :conflictReason where FilePath = :path COLLATE NOCASE ",
|
|
380 | 380 |
connection)) |
381 | 381 |
{ |
382 | 382 |
|
383 | 383 |
command.Parameters.AddWithValue("path", absolutePath); |
384 | 384 |
command.Parameters.AddWithValue("fileStatus", fileStatus); |
385 | 385 |
command.Parameters.AddWithValue("overlayStatus", overlayStatus); |
386 |
command.Parameters.AddWithValue("conflictReason", conflictReason); |
|
386 | 387 |
|
387 | 388 |
var affected = command.ExecuteNonQuery(); |
388 | 389 |
if (affected == 0) |
... | ... | |
390 | 391 |
var createdState=FileState.CreateFor(FileInfoExtensions.FromPath(absolutePath)); |
391 | 392 |
createdState.FileStatus = fileStatus; |
392 | 393 |
createdState.OverlayStatus = overlayStatus; |
394 |
createdState.ConflictReason = conflictReason; |
|
393 | 395 |
createdState.Create(); |
394 | 396 |
} |
395 | 397 |
return affected; |
... | ... | |
582 | 584 |
_persistenceAgent.Post(() =>FileState.RenameState(oldPath, newPath)); |
583 | 585 |
}*/ |
584 | 586 |
|
585 |
public void SetFileState(string path, FileStatus fileStatus, FileOverlayStatus overlayStatus, string localFileMissingFromServer)
|
|
587 |
public void SetFileState(string path, FileStatus fileStatus, FileOverlayStatus overlayStatus, string conflictReason)
|
|
586 | 588 |
{ |
587 | 589 |
if (String.IsNullOrWhiteSpace(path)) |
588 | 590 |
throw new ArgumentNullException("path"); |
... | ... | |
593 | 595 |
Debug.Assert(!path.Contains(FolderConstants.CacheFolder)); |
594 | 596 |
Debug.Assert(!path.EndsWith(".ignore")); |
595 | 597 |
|
596 |
_persistenceAgent.Post(() => UpdateStatusDirect(path, fileStatus, overlayStatus)); |
|
598 |
_persistenceAgent.Post(() => UpdateStatusDirect(path, fileStatus, overlayStatus, conflictReason));
|
|
597 | 599 |
} |
598 | 600 |
|
599 | 601 |
/* |
b/trunk/Pithos.Network/CloudFilesClient.cs | ||
---|---|---|
1416 | 1416 |
client.PutWithRetry(fileUrl, 3, @"application/octet-stream"); |
1417 | 1417 |
|
1418 | 1418 |
var expectedCodes = new[] { HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created}; |
1419 |
return (expectedCodes.Contains(client.StatusCode)); |
|
1419 |
var result=(expectedCodes.Contains(client.StatusCode)); |
|
1420 |
DeleteObject(account, cloudFile.Container, fileUrl); |
|
1421 |
return result; |
|
1420 | 1422 |
} |
1421 | 1423 |
catch |
1422 | 1424 |
{ |
1423 | 1425 |
return false; |
1424 | 1426 |
} |
1425 |
finally |
|
1426 |
{ |
|
1427 |
DeleteObject(account,cloudFile.Container,fileUrl); |
|
1428 |
} |
|
1429 | 1427 |
} |
1430 | 1428 |
} |
1431 | 1429 |
} |
Also available in: Unified diff