From d9aa5d6ab22e4ad762bab8fc15948e8c14c3ad72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20Nieto?= Date: Thu, 28 Dec 2023 14:55:48 +0100 Subject: [PATCH] Add checksum based file comparison (#20) --- .../AvaloniaSyncer.Tests.csproj | 8 +- AvaloniaSyncer.sln | 12 +-- src/AvaloniaSyncer/App.axaml | 3 +- src/AvaloniaSyncer/AvaloniaSyncer.csproj | 8 +- .../Local/DisposableFileSystemRoot.cs | 1 + .../Synchronization/Actions/CopyAction.cs | 57 +++++++++++++ .../DoNothing.cs} | 29 +++---- .../Actions/FileActionFactory.cs | 84 +++++++++++++++++++ .../{ => Actions}/IFileActionViewModel.cs | 7 +- .../Synchronization/GranularSessionView.axaml | 14 +++- .../GranularSessionViewModel.cs | 41 ++++----- .../LeftOnlyFileActionViewModel.cs | 73 ---------------- .../SyncronizationSectionView.axaml | 2 +- .../ZafiroHelpers/FileToPathConverter.cs | 10 +++ 14 files changed, 217 insertions(+), 132 deletions(-) create mode 100644 src/AvaloniaSyncer/Sections/Synchronization/Actions/CopyAction.cs rename src/AvaloniaSyncer/Sections/Synchronization/{SkipFileActionViewModel.cs => Actions/DoNothing.cs} (55%) create mode 100644 src/AvaloniaSyncer/Sections/Synchronization/Actions/FileActionFactory.cs rename src/AvaloniaSyncer/Sections/Synchronization/{ => Actions}/IFileActionViewModel.cs (67%) delete mode 100644 src/AvaloniaSyncer/Sections/Synchronization/LeftOnlyFileActionViewModel.cs create mode 100644 src/AvaloniaSyncer/ZafiroHelpers/FileToPathConverter.cs diff --git a/AvaloniaSyncer.Tests/AvaloniaSyncer.Tests.csproj b/AvaloniaSyncer.Tests/AvaloniaSyncer.Tests.csproj index 0ca8dee..d13bb15 100644 --- a/AvaloniaSyncer.Tests/AvaloniaSyncer.Tests.csproj +++ b/AvaloniaSyncer.Tests/AvaloniaSyncer.Tests.csproj @@ -19,10 +19,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + diff --git a/AvaloniaSyncer.sln b/AvaloniaSyncer.sln index 0231014..bfb9357 100644 --- a/AvaloniaSyncer.sln +++ b/AvaloniaSyncer.sln @@ -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 @@ -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 diff --git a/src/AvaloniaSyncer/App.axaml b/src/AvaloniaSyncer/App.axaml index 5205ee2..392ee43 100644 --- a/src/AvaloniaSyncer/App.axaml +++ b/src/AvaloniaSyncer/App.axaml @@ -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"> @@ -81,7 +80,7 @@ diff --git a/src/AvaloniaSyncer/AvaloniaSyncer.csproj b/src/AvaloniaSyncer/AvaloniaSyncer.csproj index f55eea8..4e91160 100644 --- a/src/AvaloniaSyncer/AvaloniaSyncer.csproj +++ b/src/AvaloniaSyncer/AvaloniaSyncer.csproj @@ -27,10 +27,10 @@ - - - - + + + + \ No newline at end of file diff --git a/src/AvaloniaSyncer/Sections/Connections/Configuration/Local/DisposableFileSystemRoot.cs b/src/AvaloniaSyncer/Sections/Connections/Configuration/Local/DisposableFileSystemRoot.cs index b51acf6..fc65984 100644 --- a/src/AvaloniaSyncer/Sections/Connections/Configuration/Local/DisposableFileSystemRoot.cs +++ b/src/AvaloniaSyncer/Sections/Connections/Configuration/Local/DisposableFileSystemRoot.cs @@ -33,6 +33,7 @@ public void Dispose() public Task CreateDirectory(ZafiroPath path) => disposableFilesystemRootImplementation.CreateDirectory(path); public Task> GetFileProperties(ZafiroPath path) => disposableFilesystemRootImplementation.GetFileProperties(path); + public Task>> GetChecksums(ZafiroPath path) => disposableFilesystemRootImplementation.GetChecksums(path); public Task> GetDirectoryProperties(ZafiroPath path) => disposableFilesystemRootImplementation.GetDirectoryProperties(path); diff --git a/src/AvaloniaSyncer/Sections/Synchronization/Actions/CopyAction.cs b/src/AvaloniaSyncer/Sections/Synchronization/Actions/CopyAction.cs new file mode 100644 index 0000000..bde8ed1 --- /dev/null +++ b/src/AvaloniaSyncer/Sections/Synchronization/Actions/CopyAction.cs @@ -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 isSyncing = new(false); + + private CopyAction(CopyFileAction copyAction, Maybe comment) + { + this.copyAction = copyAction; + Progress = copyAction.Progress; + Description = $"Copy {copyAction.Source} to {copyAction.Destination}"; + Comment = comment.GetValueOrDefault(""); + LeftFile = Maybe.From(this.copyAction.Source); + RightFile = Maybe.From(this.copyAction.Destination); + } + + public string Comment { get; } + public Maybe LeftFile { get; } + public Maybe RightFile { get; } + public string Description { get; } + public bool IsIgnored => false; + [Reactive] public bool IsSynced { get; private set; } + public IObservable Progress { get; } + public IObservable IsSyncing => isSyncing.AsObservable(); + [Reactive] public string? Error { get; private set; } + public IObservable Rate => Progress.Select(x => x.Current).Rate().Select(ByteSize.FromBytes); + + public async Task 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> Create(IZafiroFile source, IZafiroFile destination, Maybe comment) + { + return CopyFileAction.Create(source, destination).Map(action => new CopyAction(action, comment)); + } +} \ No newline at end of file diff --git a/src/AvaloniaSyncer/Sections/Synchronization/SkipFileActionViewModel.cs b/src/AvaloniaSyncer/Sections/Synchronization/Actions/DoNothing.cs similarity index 55% rename from src/AvaloniaSyncer/Sections/Synchronization/SkipFileActionViewModel.cs rename to src/AvaloniaSyncer/Sections/Synchronization/Actions/DoNothing.cs index 119e935..a77673e 100644 --- a/src/AvaloniaSyncer/Sections/Synchronization/SkipFileActionViewModel.cs +++ b/src/AvaloniaSyncer/Sections/Synchronization/Actions/DoNothing.cs @@ -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 comment, Maybe leftFile, Maybe rightFile) { - FileDiff = fileDiff; Sync = StoppableCommand.Create(() => Observable.Return(Result.Success()), Maybe>.None); IsSyncing = Observable.Return(false); + Description = description; + LeftFile = leftFile; + RightFile = rightFile; + Comment = comment.GetValueOrDefault(""); } - public FileDiff FileDiff { get; } - + public StoppableCommand Sync { get; } + public string Comment { get; } + public Maybe LeftFile { get; } + public Maybe RightFile { get; } public IObservable IsSyncing { get; } - public string Error { get; } + public string Error => ""; public IObservable Rate => Observable.Never(); public bool IsIgnored { get; } = true; public bool IsSynced { get; } = true; - public string Description => $"Skip {FileDiff}"; + public string Description { get; } public IObservable Progress => Observable.Never(); - public Task Execute(CancellationToken cancellationToken) - { - return Task.FromResult(Result.Success()); - } - public StoppableCommand Sync { get; } + public Task Execute(CancellationToken cancellationToken) => Task.FromResult(Result.Success()); } \ No newline at end of file diff --git a/src/AvaloniaSyncer/Sections/Synchronization/Actions/FileActionFactory.cs b/src/AvaloniaSyncer/Sections/Synchronization/Actions/FileActionFactory.cs new file mode 100644 index 0000000..3eb3e48 --- /dev/null +++ b/src/AvaloniaSyncer/Sections/Synchronization/Actions/FileActionFactory.cs @@ -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> 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> FileAreEqual(BothDiff bothDiff) + { + Maybe 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.From(bothDiff.Left.File), + Maybe.From(bothDiff.Right.File)); + + return fileActionViewModel; + } + + private static string FormatChecksums(IDictionary leftHashes) + { + return leftHashes.Select(pair => "\t" + pair.Key + "=" + Convert.ToHexString(pair.Value)).JoinWithLines(); + } + + private Task> Delete(IZafiroFile rightFile) + { + // Implement this + return Task.FromResult(Result.Success(new DoNothing("Skip", "File only appear of the right side. Ignoring!", Maybe.None, Maybe.From(rightFile)))); + } + + private Task> CopyToDestination(IZafiroFile source) + { + return CopyAction.Create(source, source.EquivalentIn(destination), $"File {source} does not exist in {destination}").Cast(action => (IFileActionViewModel)action); + } + + private static Task> 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)); + } +} \ No newline at end of file diff --git a/src/AvaloniaSyncer/Sections/Synchronization/IFileActionViewModel.cs b/src/AvaloniaSyncer/Sections/Synchronization/Actions/IFileActionViewModel.cs similarity index 67% rename from src/AvaloniaSyncer/Sections/Synchronization/IFileActionViewModel.cs rename to src/AvaloniaSyncer/Sections/Synchronization/Actions/IFileActionViewModel.cs index 25fef91..8230fb8 100644 --- a/src/AvaloniaSyncer/Sections/Synchronization/IFileActionViewModel.cs +++ b/src/AvaloniaSyncer/Sections/Synchronization/Actions/IFileActionViewModel.cs @@ -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 { @@ -15,4 +17,7 @@ public interface IFileActionViewModel : INotifyPropertyChanged, IFileAction public IObservable IsSyncing { get; } public string? Error { get; } public IObservable Rate { get; } + string Comment { get; } + Maybe LeftFile { get; } + Maybe RightFile { get; } } \ No newline at end of file diff --git a/src/AvaloniaSyncer/Sections/Synchronization/GranularSessionView.axaml b/src/AvaloniaSyncer/Sections/Synchronization/GranularSessionView.axaml index c4983b0..65c8410 100644 --- a/src/AvaloniaSyncer/Sections/Synchronization/GranularSessionView.axaml +++ b/src/AvaloniaSyncer/Sections/Synchronization/GranularSessionView.axaml @@ -11,7 +11,7 @@ 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 - + @@ -22,9 +22,17 @@ IsVisible="{Binding IsSyncing^}" VerticalAlignment="Stretch" Height="20" DockPanel.Dock="Bottom" Margin="4" Maximum="{Binding Progress^.Total}" Value="{Binding Progress^.Current}" /> - + - + + + + + + + + + diff --git a/src/AvaloniaSyncer/Sections/Synchronization/GranularSessionViewModel.cs b/src/AvaloniaSyncer/Sections/Synchronization/GranularSessionViewModel.cs index eee5b14..1f0c82f 100644 --- a/src/AvaloniaSyncer/Sections/Synchronization/GranularSessionViewModel.cs +++ b/src/AvaloniaSyncer/Sections/Synchronization/GranularSessionViewModel.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel; using System.Linq; using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Threading.Tasks; +using AvaloniaSyncer.Sections.Synchronization.Actions; using CSharpFunctionalExtensions; using DynamicData; using ReactiveUI; @@ -28,27 +30,29 @@ public GranularSessionViewModel(IZafiroDirectory source, IZafiroDirectory destin Source = source; Destination = destination; - var sourceList = new SourceList(); - var sourceListChanges = sourceList + var syncActionsList = new SourceList(); + var sourceListChanges = syncActionsList .Connect(); sourceListChanges - .Bind(out var actions) + .Bind(out var syncActions) .Subscribe(); + SyncActions = syncActions; + var pendingSync = sourceListChanges .AutoRefresh() .Filter(x => x is { IsIgnored: false, IsSynced: false }); ISubject canAnalyze = new Subject(); - Analyze = StoppableCommand.Create(() => Observable.FromAsync(() => new FileSystemComparer().Diff(source, destination)), Maybe.From(canAnalyze.AsObservable())); + Analyze = StoppableCommand.Create(() => Observable.FromAsync(() => new FileSystemComparer2().Diff(source, destination)), Maybe.From(canAnalyze.AsObservable())); var canSync = pendingSync.ToCollection().Any(); - Actions = actions; + SyncAll = StoppableCommand.Create(() => { return Observable.FromAsync(async ct => { - var compositeAction = new CompositeAction(Actions.Where(x => !x.IsIgnored).Cast>().ToList()); + var compositeAction = new CompositeAction(SyncActions.Where(x => !x.IsIgnored).Cast>().ToList()); using (compositeAction.Progress.Subscribe(progress)) { return await compositeAction.Execute(ct); @@ -56,7 +60,7 @@ public GranularSessionViewModel(IZafiroDirectory source, IZafiroDirectory destin }); }, Maybe.From(canSync)); SyncAll.IsExecuting.Not().Subscribe(canAnalyze); - ItemsUpdater(sourceList, Analyze.Start.Successes()).Subscribe(); + ItemsUpdater(syncActionsList, Analyze.Start.Successes()).Subscribe(); IsSyncing = SyncAll.IsExecuting; } @@ -64,39 +68,28 @@ public GranularSessionViewModel(IZafiroDirectory source, IZafiroDirectory destin public StoppableCommand SyncAll { get; } public IZafiroDirectory Source { get; } public IZafiroDirectory Destination { get; } - - public ReadOnlyObservableCollection Actions { get; } - + public ReadOnlyObservableCollection SyncActions { get; } public StoppableCommand>> Analyze { get; } public string Description => $"{Source} => {Destination}"; public IObservable IsSyncing { get; } - private IObservable> ItemsUpdater(ISourceList sourceList, IObservable> listsOfDiffs) + private IObservable> ItemsUpdater(ISourceList actions, IObservable> listsOfDiffs) { var observableOfLists = listsOfDiffs .SelectMany(diffs => diffs .ToObservable() - .Select(diff => Observable.FromAsync(() => GenerateAction(diff))) + .Select(diff => Observable.FromAsync(() => GenerateActionFor(diff))) .Merge(3) .Successes().ToList()); return observableOfLists .ObserveOn(RxApp.MainThreadScheduler) - .Do(list => - { - sourceList.EditDiff(list); - }); + .Do(acts => actions.EditDiff(acts)); } - private Task> GenerateAction(FileDiff fileDiff) + private Task> GenerateActionFor(FileDiff fileDiff) { - return fileDiff switch - { - Both both => Task.FromResult(Result.Success(new SkipFileActionViewModel(both))).Cast(model => (IFileActionViewModel)model), - RightOnly rightOnly => Task.FromResult(Result.Success(new SkipFileActionViewModel(rightOnly))).Cast(model => (IFileActionViewModel)model), - LeftOnly leftOnly => LeftOnlyFileActionViewModel.Create(leftOnly.Left.Path, Source, Destination).Cast(model => (IFileActionViewModel)model), - _ => throw new ArgumentOutOfRangeException(nameof(fileDiff)) - }; + return new FileActionFactory(Destination).Create(fileDiff); } } \ No newline at end of file diff --git a/src/AvaloniaSyncer/Sections/Synchronization/LeftOnlyFileActionViewModel.cs b/src/AvaloniaSyncer/Sections/Synchronization/LeftOnlyFileActionViewModel.cs deleted file mode 100644 index 2a4f28b..0000000 --- a/src/AvaloniaSyncer/Sections/Synchronization/LeftOnlyFileActionViewModel.cs +++ /dev/null @@ -1,73 +0,0 @@ -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.CSharpFunctionalExtensions; -using Zafiro.FileSystem; -using Zafiro.FileSystem.Actions; -using Zafiro.Mixins; - -namespace AvaloniaSyncer.Sections.Synchronization; - -internal class LeftOnlyFileActionViewModel : ReactiveObject, IFileActionViewModel -{ - private readonly CopyFileAction copyFileAction; - private readonly BehaviorSubject isSyncing = new(false); - - public LeftOnlyFileActionViewModel(ZafiroPath left, IZafiroDirectory source, IZafiroDirectory destination, CopyFileAction copyFileAction) - { - this.copyFileAction = copyFileAction; - Left = left; - Source = source; - Destination = destination; - Progress = copyFileAction.Progress; - } - - public ZafiroPath Left { get; } - public IZafiroDirectory Source { get; } - public IZafiroDirectory Destination { get; } - - public bool IsIgnored { get; } = false; - - [Reactive] public bool IsSynced { get; private set; } - - public string Description => $"[Copy] {Left}"; - public IObservable Progress { get; } - public IObservable IsSyncing => isSyncing.AsObservable(); - - public async Task Execute(CancellationToken cancellationToken) - { - isSyncing.OnNext(true); - var execute = await copyFileAction.Execute(cancellationToken); - isSyncing.OnNext(false); - execute.TapError(e => Error = e); - execute.Tap(() => IsSynced = true); - return execute; - } - - [Reactive] - public string? Error { get; private set; } - - public IObservable Rate => Progress.Select(x => x.Current).Rate().Select(ByteSize.FromBytes); - - public override string ToString() - { - return $"{nameof(Left)}: {Left}, {nameof(Source)}: {Source}, {nameof(Destination)}: {Destination}"; - } - - public static Task> Create(ZafiroPath left, IZafiroDirectory source, IZafiroDirectory destination) - { - return source.GetFromPath(left).CombineAndBind(destination.GetFromPath(left), (src, dst) => - { - return CopyFileAction - .Create(src, dst) - .Map(action => new LeftOnlyFileActionViewModel(left, source, destination, action)); - }); - } -} \ No newline at end of file diff --git a/src/AvaloniaSyncer/Sections/Synchronization/SyncronizationSectionView.axaml b/src/AvaloniaSyncer/Sections/Synchronization/SyncronizationSectionView.axaml index d076b3d..3b139ba 100644 --- a/src/AvaloniaSyncer/Sections/Synchronization/SyncronizationSectionView.axaml +++ b/src/AvaloniaSyncer/Sections/Synchronization/SyncronizationSectionView.axaml @@ -11,7 +11,7 @@ - + diff --git a/src/AvaloniaSyncer/ZafiroHelpers/FileToPathConverter.cs b/src/AvaloniaSyncer/ZafiroHelpers/FileToPathConverter.cs new file mode 100644 index 0000000..a22d4e5 --- /dev/null +++ b/src/AvaloniaSyncer/ZafiroHelpers/FileToPathConverter.cs @@ -0,0 +1,10 @@ +using Avalonia.Data.Converters; +using CSharpFunctionalExtensions; +using Zafiro.FileSystem; + +namespace AvaloniaSyncer.ZafiroHelpers; + +public class MiscConverters +{ + public static FuncValueConverter, string> MaybeZafiroFileToPath => new(file => file.Map(r => r.Path.ToString()).GetValueOrDefault()); +} \ No newline at end of file