Skip to content

Commit

Permalink
Add checksum based file comparison (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
SuperJMN authored Dec 28, 2023
1 parent 8eba828 commit d9aa5d6
Show file tree
Hide file tree
Showing 14 changed files with 217 additions and 132 deletions.
8 changes: 4 additions & 4 deletions AvaloniaSyncer.Tests/AvaloniaSyncer.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Zafiro.FileSystem" Version="5.0.1" />
<PackageReference Include="Zafiro.FileSystem.Local" Version="5.0.1" />
<PackageReference Include="Zafiro.FileSystem.SeaweedFS" Version="5.0.1" />
<PackageReference Include="Zafiro.FileSystem.Sftp" Version="5.0.1" />
<PackageReference Include="Zafiro.FileSystem" Version="5.0.2" />
<PackageReference Include="Zafiro.FileSystem.Local" Version="5.0.2" />
<PackageReference Include="Zafiro.FileSystem.SeaweedFS" Version="5.0.2" />
<PackageReference Include="Zafiro.FileSystem.Sftp" Version="5.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\src\AvaloniaSyncer\AvaloniaSyncer.csproj" />
Expand Down
12 changes: 6 additions & 6 deletions AvaloniaSyncer.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvaloniaSyncer.Desktop", "s
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvaloniaSyncer.Tests", "AvaloniaSyncer.Tests\AvaloniaSyncer.Tests.csproj", "{258C4207-A2C7-4BF3-9266-45365ECDEAB6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvaloniaSyncer", "src\AvaloniaSyncer\AvaloniaSyncer.csproj", "{C4175539-20BE-4FBC-9AD5-52BF54294394}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{44B49082-C9E6-48D3-9F89-6E2CE99759B0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvaloniaSyncer", "src\AvaloniaSyncer\AvaloniaSyncer.csproj", "{975C2AED-D262-4970-AD82-AF4AF15167C3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -33,14 +33,14 @@ Global
{258C4207-A2C7-4BF3-9266-45365ECDEAB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{258C4207-A2C7-4BF3-9266-45365ECDEAB6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{258C4207-A2C7-4BF3-9266-45365ECDEAB6}.Release|Any CPU.Build.0 = Release|Any CPU
{C4175539-20BE-4FBC-9AD5-52BF54294394}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C4175539-20BE-4FBC-9AD5-52BF54294394}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C4175539-20BE-4FBC-9AD5-52BF54294394}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C4175539-20BE-4FBC-9AD5-52BF54294394}.Release|Any CPU.Build.0 = Release|Any CPU
{44B49082-C9E6-48D3-9F89-6E2CE99759B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{44B49082-C9E6-48D3-9F89-6E2CE99759B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{44B49082-C9E6-48D3-9F89-6E2CE99759B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{44B49082-C9E6-48D3-9F89-6E2CE99759B0}.Release|Any CPU.Build.0 = Release|Any CPU
{975C2AED-D262-4970-AD82-AF4AF15167C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{975C2AED-D262-4970-AD82-AF4AF15167C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{975C2AED-D262-4970-AD82-AF4AF15167C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{975C2AED-D262-4970-AD82-AF4AF15167C3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
3 changes: 1 addition & 2 deletions src/AvaloniaSyncer/App.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
xmlns:misc="clr-namespace:Zafiro.Avalonia.Misc;assembly=Zafiro.Avalonia"
xmlns:controls="clr-namespace:Zafiro.Avalonia.Controls;assembly=Zafiro.Avalonia"
xmlns:wizard="clr-namespace:Zafiro.Avalonia.Wizard;assembly=Zafiro.Avalonia"
xmlns:e="clr-namespace:AvaloniaSyncer.Sections.Explorer"
x:Class="AvaloniaSyncer.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
Expand Down Expand Up @@ -81,7 +80,7 @@

<Style Selector="TabItem">
<Setter Property="MinHeight" Value="27" />
<Setter Property="MaxWidth" Value="120" />
<Setter Property="MaxWidth" Value="250" />
<Setter Property="TextElement.FontSize" Value="16" />
<Setter Property="BorderBrush" Value="{StaticResource TabItemBorderBrush}" />
</Style>
Expand Down
8 changes: 4 additions & 4 deletions src/AvaloniaSyncer/AvaloniaSyncer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Zafiro.Avalonia" Version="3.0.66" />
<PackageReference Include="Zafiro.Avalonia.Dialogs" Version="3.0.66" />
<PackageReference Include="Zafiro.Avalonia.FileExplorer" Version="1.0.30" />
<PackageReference Include="Zafiro.FileSystem.Local" Version="5.0.1" />
<PackageReference Include="Zafiro.FileSystem.SeaweedFS" Version="5.0.1" />
<PackageReference Include="Zafiro.FileSystem.Sftp" Version="5.0.1" />
<PackageReference Include="Zafiro.Avalonia.FileExplorer" Version="1.0.31" />
<PackageReference Include="Zafiro.FileSystem.Local" Version="5.0.2" />
<PackageReference Include="Zafiro.FileSystem.SeaweedFS" Version="5.0.2" />
<PackageReference Include="Zafiro.FileSystem.Sftp" Version="5.0.2" />
<PackageReference Include="Zafiro.UI" Version="4.0.19" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public void Dispose()
public Task<Result> CreateDirectory(ZafiroPath path) => disposableFilesystemRootImplementation.CreateDirectory(path);

public Task<Result<FileProperties>> GetFileProperties(ZafiroPath path) => disposableFilesystemRootImplementation.GetFileProperties(path);
public Task<Result<IDictionary<ChecksumKind, byte[]>>> GetChecksums(ZafiroPath path) => disposableFilesystemRootImplementation.GetChecksums(path);

public Task<Result<DirectoryProperties>> GetDirectoryProperties(ZafiroPath path) => disposableFilesystemRootImplementation.GetDirectoryProperties(path);

Expand Down
57 changes: 57 additions & 0 deletions src/AvaloniaSyncer/Sections/Synchronization/Actions/CopyAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading;
using System.Threading.Tasks;
using ByteSizeLib;
using CSharpFunctionalExtensions;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Zafiro.Actions;
using Zafiro.FileSystem;
using Zafiro.FileSystem.Actions;
using Zafiro.Mixins;

namespace AvaloniaSyncer.Sections.Synchronization.Actions;

public class CopyAction : ReactiveObject, IFileActionViewModel
{
private readonly CopyFileAction copyAction;
private readonly BehaviorSubject<bool> isSyncing = new(false);

private CopyAction(CopyFileAction copyAction, Maybe<string> comment)
{
this.copyAction = copyAction;
Progress = copyAction.Progress;
Description = $"Copy {copyAction.Source} to {copyAction.Destination}";
Comment = comment.GetValueOrDefault("");
LeftFile = Maybe<IZafiroFile>.From(this.copyAction.Source);
RightFile = Maybe<IZafiroFile>.From(this.copyAction.Destination);
}

public string Comment { get; }
public Maybe<IZafiroFile> LeftFile { get; }
public Maybe<IZafiroFile> RightFile { get; }
public string Description { get; }
public bool IsIgnored => false;
[Reactive] public bool IsSynced { get; private set; }
public IObservable<LongProgress> Progress { get; }
public IObservable<bool> IsSyncing => isSyncing.AsObservable();
[Reactive] public string? Error { get; private set; }
public IObservable<ByteSize> Rate => Progress.Select(x => x.Current).Rate().Select(ByteSize.FromBytes);

public async Task<Result> Execute(CancellationToken cancellationToken)
{
isSyncing.OnNext(true);
var execute = await copyAction.Execute(cancellationToken);
isSyncing.OnNext(false);
execute.TapError(e => Error = e);
execute.Tap(() => IsSynced = true);
return execute;
}

public static Task<Result<CopyAction>> Create(IZafiroFile source, IZafiroFile destination, Maybe<string> comment)
{
return CopyFileAction.Create(source, destination).Map(action => new CopyAction(action, comment));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,34 @@
using CSharpFunctionalExtensions;
using ReactiveUI;
using Zafiro.Actions;
using Zafiro.FileSystem.Comparer;
using Zafiro.FileSystem;
using Zafiro.UI;

namespace AvaloniaSyncer.Sections.Synchronization;
namespace AvaloniaSyncer.Sections.Synchronization.Actions;

internal class SkipFileActionViewModel : ReactiveObject, IFileActionViewModel
internal class DoNothing : ReactiveObject, IFileActionViewModel
{
public SkipFileActionViewModel(FileDiff fileDiff)
public DoNothing(string description, Maybe<string> comment, Maybe<IZafiroFile> leftFile, Maybe<IZafiroFile> rightFile)
{
FileDiff = fileDiff;
Sync = StoppableCommand.Create(() => Observable.Return(Result.Success()), Maybe<IObservable<bool>>.None);
IsSyncing = Observable.Return(false);
Description = description;
LeftFile = leftFile;
RightFile = rightFile;
Comment = comment.GetValueOrDefault("");
}

public FileDiff FileDiff { get; }

public StoppableCommand<Unit, Result> Sync { get; }
public string Comment { get; }
public Maybe<IZafiroFile> LeftFile { get; }
public Maybe<IZafiroFile> RightFile { get; }
public IObservable<bool> IsSyncing { get; }
public string Error { get; }
public string Error => "";
public IObservable<ByteSize> Rate => Observable.Never<ByteSize>();
public bool IsIgnored { get; } = true;
public bool IsSynced { get; } = true;
public string Description => $"Skip {FileDiff}";
public string Description { get; }
public IObservable<LongProgress> Progress => Observable.Never<LongProgress>();
public Task<Result> Execute(CancellationToken cancellationToken)
{
return Task.FromResult(Result.Success());
}

public StoppableCommand<Unit, Result> Sync { get; }
public Task<Result> Execute(CancellationToken cancellationToken) => Task.FromResult(Result.Success());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CSharpFunctionalExtensions;
using Zafiro.CSharpFunctionalExtensions;
using Zafiro.FileSystem;
using Zafiro.FileSystem.Comparer;
using Zafiro.Mixins;

namespace AvaloniaSyncer.Sections.Synchronization.Actions;

public class FileActionFactory
{
private readonly IZafiroDirectory destination;

public FileActionFactory(IZafiroDirectory destination)
{
this.destination = destination;
}

public Task<Result<IFileActionViewModel>> Create(FileDiff diff)
{

return diff switch
{
BothDiff bothDiff => AreEquivalent(bothDiff.Left, bothDiff.Right) ? FileAreEqual(bothDiff) : FileAreDifferent(bothDiff.Left.File, bothDiff.Right.File),
LeftOnlyDiff leftOnlyDiff => CopyToDestination(leftOnlyDiff.Left.File),
RightOnlyDiff rightOnlyDiff => Delete(rightOnlyDiff.Right.File),
_ => throw new ArgumentOutOfRangeException(nameof(diff))
};
}

private static async Task<Result<IFileActionViewModel>> FileAreEqual(BothDiff bothDiff)
{
Maybe<string> comment = $"""
One of the checksums match:
· {bothDiff.Left.File}:
{FormatChecksums(bothDiff.Left.Hashes)}
· {bothDiff.Right.File}:
{FormatChecksums(bothDiff.Left.Hashes)}
""";

var fileActionViewModel = new DoNothing(
"Skip",
comment,
Maybe<IZafiroFile>.From(bothDiff.Left.File),
Maybe<IZafiroFile>.From(bothDiff.Right.File));

return fileActionViewModel;
}

private static string FormatChecksums(IDictionary<ChecksumKind, byte[]> leftHashes)
{
return leftHashes.Select(pair => "\t" + pair.Key + "=" + Convert.ToHexString(pair.Value)).JoinWithLines();
}

private Task<Result<IFileActionViewModel>> Delete(IZafiroFile rightFile)
{
// Implement this
return Task.FromResult(Result.Success<IFileActionViewModel>(new DoNothing("Skip", "File only appear of the right side. Ignoring!", Maybe<IZafiroFile>.None, Maybe.From(rightFile))));
}

private Task<Result<IFileActionViewModel>> CopyToDestination(IZafiroFile source)
{
return CopyAction.Create(source, source.EquivalentIn(destination), $"File {source} does not exist in {destination}").Cast(action => (IFileActionViewModel)action);
}

private static Task<Result<IFileActionViewModel>> FileAreDifferent(IZafiroFile source, IZafiroFile destination)
{
return CopyAction.Create(source, destination, "Files are different").Cast(action => (IFileActionViewModel)action);
}

private static bool AreEquivalent(FileWithMetadata left, FileWithMetadata right)
{
var hashCombinations = from leftHash in left.Hashes
join rightHash in right.Hashes on leftHash.Key equals rightHash.Key
select new { LeftHash = leftHash, RightHash = rightHash };

return hashCombinations.Any(combination =>
StructuralComparisons.StructuralEqualityComparer.Equals(combination.LeftHash.Value, combination.RightHash.Value));
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System;
using System.ComponentModel;
using ByteSizeLib;
using CSharpFunctionalExtensions;
using Zafiro.Actions;
using Zafiro.FileSystem;
using Zafiro.FileSystem.Actions;

namespace AvaloniaSyncer.Sections.Synchronization;
namespace AvaloniaSyncer.Sections.Synchronization.Actions;

public interface IFileActionViewModel : INotifyPropertyChanged, IFileAction
{
Expand All @@ -15,4 +17,7 @@ public interface IFileActionViewModel : INotifyPropertyChanged, IFileAction
public IObservable<bool> IsSyncing { get; }
public string? Error { get; }
public IObservable<ByteSize> Rate { get; }
string Comment { get; }
Maybe<IZafiroFile> LeftFile { get; }
Maybe<IZafiroFile> RightFile { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

<UserControl.Resources>
<StreamGeometry x:Key="Warning">M10.9085 2.78216C11.9483 2.20625 13.2463 2.54089 13.8841 3.5224L13.9669 3.66023L21.7259 17.6685C21.9107 18.0021 22.0076 18.3773 22.0076 18.7587C22.0076 19.9495 21.0825 20.9243 19.9117 21.0035L19.7576 21.0087H4.24187C3.86056 21.0087 3.4855 20.9118 3.15192 20.7271C2.11208 20.1513 1.70704 18.8734 2.20059 17.812L2.27349 17.6687L10.0303 3.66046C10.2348 3.2911 10.5391 2.98674 10.9085 2.78216ZM12.0004 16.0018C11.4489 16.0018 11.0018 16.4489 11.0018 17.0004C11.0018 17.552 11.4489 17.9991 12.0004 17.9991C12.552 17.9991 12.9991 17.552 12.9991 17.0004C12.9991 16.4489 12.552 16.0018 12.0004 16.0018ZM11.9983 7.99806C11.4854 7.99825 11.0629 8.38444 11.0053 8.8818L10.9986 8.99842L11.0004 13.9993L11.0072 14.1159C11.0652 14.6132 11.488 14.9991 12.0008 14.9989C12.5136 14.9988 12.9362 14.6126 12.9938 14.1152L13.0004 13.9986L12.9986 8.9977L12.9919 8.88108C12.9339 8.38376 12.5111 7.99788 11.9983 7.99806Z</StreamGeometry>
</UserControl.Resources>
</UserControl.Resources>

<DockPanel>
<StackPanel Orientation="Horizontal" Spacing="8" DockPanel.Dock="Top">
Expand All @@ -22,9 +22,17 @@
IsVisible="{Binding IsSyncing^}"
VerticalAlignment="Stretch" Height="20" DockPanel.Dock="Bottom" Margin="4" Maximum="{Binding Progress^.Total}"
Value="{Binding Progress^.Current}" />
<DataGrid ItemsSource="{Binding Actions}" CanUserResizeColumns="True" CanUserSortColumns="True">
<DataGrid ItemsSource="{Binding SyncActions}" CanUserResizeColumns="True" CanUserSortColumns="True">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Description}" Header="Action" />
<DataGridTemplateColumn Header="Action">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock HorizontalAlignment="Center" FontWeight="Bold" VerticalAlignment="Center" Text="{Binding Description}" ToolTip.Tip="{Binding Comment}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Binding="{Binding LeftFile, Converter={x:Static MiscConverters.MaybeZafiroFileToPath}}" Header="Source" />
<DataGridTextColumn Binding="{Binding RightFile, Converter={x:Static MiscConverters.MaybeZafiroFileToPath}}" Header="Destination" />
<DataGridCheckBoxColumn Header="IsSynced" Binding="{Binding IsSynced}" />
<DataGridTemplateColumn Header="Error">
<DataGridTemplateColumn.CellTemplate>
Expand Down
Loading

0 comments on commit d9aa5d6

Please sign in to comment.