Merge pull request #412 from Mbucari/master

Fix Character Replacements and Add More Useful Error Messages
This commit is contained in:
rmcrackan 2022-12-16 21:28:36 -05:00 committed by GitHub
commit 19860e9f09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1913 additions and 1432 deletions

View File

@ -105,7 +105,7 @@ namespace FileManager
// Other illegal characters will be taken care of later. Must take care of slashes now so params can't introduce new folders.
// Esp important for file templates.
return replacements.ReplaceInvalidFilenameChars(value.ToString());
return replacements.ReplaceFilenameChars(value.ToString());
}
}
}

View File

@ -84,7 +84,7 @@ namespace FileManager
var pathNoPrefix = path.PathWithoutPrefix;
pathNoPrefix = replacements.ReplaceInvalidPathChars(pathNoPrefix);
pathNoPrefix = replacements.ReplacePathChars(pathNoPrefix);
pathNoPrefix = removeDoubleSlashes(pathNoPrefix);
return pathNoPrefix;

View File

@ -61,9 +61,15 @@ namespace FileManager
[JsonConverter(typeof(ReplacementCharactersConverter))]
public class ReplacementCharacters
{
public static readonly ReplacementCharacters Default = new()
static ReplacementCharacters()
{
Replacements = new List<Replacement>()
}
public static readonly ReplacementCharacters Default
= IsWindows
? new()
{
Replacements = new Replacement[]
{
Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash(""),
@ -78,11 +84,25 @@ namespace FileManager
Replacement.QuestionMark(""),
Replacement.Pipe("⏐"),
}
}
: new()
{
Replacements = new Replacement[]
{
Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash(""),
Replacement.FilenameBackSlash("\\"),
Replacement.OpenQuote("“"),
Replacement.CloseQuote("”"),
Replacement.OtherQuote("\"")
}
};
public static readonly ReplacementCharacters LoFiDefault = new()
public static readonly ReplacementCharacters LoFiDefault
= IsWindows
? new()
{
Replacements = new List<Replacement>()
Replacements = new Replacement[]
{
Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash("_"),
@ -94,26 +114,55 @@ namespace FileManager
Replacement.CloseAngleBracket("}"),
Replacement.Colon("-"),
}
}
: new ()
{
Replacements = new Replacement[]
{
Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash("_"),
Replacement.FilenameBackSlash("\\"),
Replacement.OpenQuote("\""),
Replacement.CloseQuote("\""),
Replacement.OtherQuote("\"")
}
};
public static readonly ReplacementCharacters Barebones = new()
public static readonly ReplacementCharacters Barebones
= IsWindows
? new ()
{
Replacements = new List<Replacement>()
Replacements = new Replacement[]
{
Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash("_"),
Replacement.FilenameBackSlash("_"),
Replacement.OpenQuote("_"),
Replacement.CloseQuote("_"),
Replacement.OtherQuote("_"),
Replacement.OtherQuote("_")
}
}
: new ()
{
Replacements = new Replacement[]
{
Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash("_"),
Replacement.FilenameBackSlash("\\"),
Replacement.OpenQuote("\""),
Replacement.CloseQuote("\""),
Replacement.OtherQuote("\"")
}
};
private static readonly char[] invalidChars = Path.GetInvalidPathChars().Union(new[] {
'*', '?', ':',
// these are weird. If you run Path.GetInvalidPathChars() in Visual Studio's "C# Interactive", then these characters are included.
// In live code, Path.GetInvalidPathChars() does not include them
'"', '<', '>'
private static bool IsWindows => Environment.OSVersion.Platform is PlatformID.Win32NT;
private static readonly char[] invalidPathChars = Path.GetInvalidFileNameChars().Except(new[] {
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar
}).ToArray();
private static readonly char[] invalidSlashes = Path.GetInvalidFileNameChars().Intersect(new[] {
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar
}).ToArray();
public IReadOnlyList<Replacement> Replacements { get; init; }
@ -158,6 +207,10 @@ namespace FileManager
return OtherQuote;
}
if (!IsWindows && toReplace == BackSlash.CharacterToReplace)
return BackSlash.ReplacementString;
//Replace any other non-mandatory characters
for (int i = Replacement.FIXED_COUNT; i < Replacements.Count; i++)
{
var r = Replacements[i];
@ -167,13 +220,12 @@ namespace FileManager
return DefaultReplacement;
}
public static bool ContainsInvalidPathChar(string path)
=> path.Any(c => invalidChars?.Contains(c) == true);
=> path.Any(c => invalidPathChars.Contains(c));
public static bool ContainsInvalidFilenameChar(string path)
=> path.Any(c => invalidChars?.Concat(new char[] { '\\', '/' })?.Contains(c) == true);
=> ContainsInvalidPathChar(path) || path.Any(c => invalidSlashes.Contains(c));
public string ReplaceInvalidFilenameChars(string fileName)
public string ReplaceFilenameChars(string fileName)
{
if (string.IsNullOrEmpty(fileName)) return string.Empty;
var builder = new System.Text.StringBuilder();
@ -181,7 +233,9 @@ namespace FileManager
{
var c = fileName[i];
if (invalidChars.Contains(c) || c == ForwardSlash.CharacterToReplace || c == BackSlash.CharacterToReplace)
if (invalidPathChars.Contains(c)
|| invalidSlashes.Contains(c)
|| Replacements.Any(r => r.CharacterToReplace == c) /* Replace any other legal characters that they user wants. */ )
{
char preceding = i > 0 ? fileName[i - 1] : default;
char succeeding = i < fileName.Length - 1 ? fileName[i + 1] : default;
@ -189,30 +243,42 @@ namespace FileManager
}
else
builder.Append(c);
}
return builder.ToString();
}
public string ReplaceInvalidPathChars(string pathStr)
public string ReplacePathChars(string pathStr)
{
if (string.IsNullOrEmpty(pathStr)) return string.Empty;
// replace all colons except within the first 2 chars
var builder = new System.Text.StringBuilder();
for (var i = 0; i < pathStr.Length; i++)
{
var c = pathStr[i];
if (!invalidChars.Contains(c) || (c == ':' && i == 1 && Path.IsPathRooted(pathStr)))
builder.Append(c);
else
if (
(
invalidPathChars.Contains(c)
|| ( // Replace any other legal characters that they user wants.
c != Path.DirectorySeparatorChar
&& c != Path.AltDirectorySeparatorChar
&& Replacements.Any(r => r.CharacterToReplace == c)
)
)
&& !( // replace all colons except drive letter designator on Windows
c == ':'
&& i == 1
&& Path.IsPathRooted(pathStr)
&& IsWindows
)
)
{
char preceding = i > 0 ? pathStr[i - 1] : default;
char succeeding = i < pathStr.Length - 1 ? pathStr[i + 1] : default;
builder.Append(GetPathCharReplacement(c, preceding, succeeding));
}
else
builder.Append(c);
}
return builder.ToString();
}
@ -234,28 +300,19 @@ namespace FileManager
//Ensure that the first 6 replacements are for the expected chars and that all replacement strings are valid.
//If not, reset to default.
var default0 = Replacement.OtherInvalid("");
var default1 = Replacement.FilenameForwardSlash("");
var default2 = Replacement.FilenameBackSlash("");
var default3 = Replacement.OpenQuote("");
var default4 = Replacement.CloseQuote("");
var default5 = Replacement.OtherQuote("");
if (dict.Count < Replacement.FIXED_COUNT ||
dict[0].CharacterToReplace != default0.CharacterToReplace || dict[0].Description != default0.Description ||
dict[1].CharacterToReplace != default1.CharacterToReplace || dict[1].Description != default1.Description ||
dict[2].CharacterToReplace != default2.CharacterToReplace || dict[2].Description != default2.Description ||
dict[3].CharacterToReplace != default3.CharacterToReplace || dict[3].Description != default3.Description ||
dict[4].CharacterToReplace != default4.CharacterToReplace || dict[4].Description != default4.Description ||
dict[5].CharacterToReplace != default5.CharacterToReplace || dict[5].Description != default5.Description ||
dict.Any(r => ReplacementCharacters.ContainsInvalidPathChar(r.ReplacementString))
)
for (int i = 0; i < Replacement.FIXED_COUNT; i++)
{
if (dict.Count < Replacement.FIXED_COUNT
|| dict[i].CharacterToReplace != ReplacementCharacters.Barebones.Replacements[i].CharacterToReplace
|| dict[i].Description != ReplacementCharacters.Barebones.Replacements[i].Description)
{
dict = ReplacementCharacters.Default.Replacements;
break;
}
//First FIXED_COUNT are mandatory
for (int i = 0; i < Replacement.FIXED_COUNT; i++)
dict[i].Mandatory = true;
}
return new ReplacementCharacters { Replacements = dict };
}
@ -265,7 +322,7 @@ namespace FileManager
ReplacementCharacters replacements = (ReplacementCharacters)value;
var propertyNames = replacements.Replacements
.Select(c => JObject.FromObject(c)).ToList();
.Select(JObject.FromObject).ToList();
var prop = new JProperty(nameof(Replacement), new JArray(propertyNames));

View File

@ -17,10 +17,12 @@ namespace HangoverAvalonia
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
var mainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
DataContext = new MainVM(),
};
desktop.MainWindow = mainWindow;
mainWindow.OnLoad();
}
base.OnFrameworkInitializationCompleted();

View File

@ -0,0 +1,30 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="HangoverAvalonia.Controls.CheckedListBox">
<UserControl.Resources>
<RecyclePool x:Key="RecyclePool" />
<DataTemplate x:Key="queuedBook">
<CheckBox Margin="10,0,0,0" Content="{Binding Item}" IsChecked="{Binding IsChecked, Mode=TwoWay}" />
</DataTemplate>
<RecyclingElementFactory x:Key="elementFactory" RecyclePool="{StaticResource RecyclePool}">
<RecyclingElementFactory.Templates>
<StaticResource x:Key="queuedBook" ResourceKey="queuedBook" />
</RecyclingElementFactory.Templates>
</RecyclingElementFactory>
</UserControl.Resources>
<ScrollViewer
Name="scroller"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ItemsRepeater IsVisible="True"
VerticalCacheLength="1.2"
HorizontalCacheLength="1"
Items="{Binding CheckboxItems}"
ItemTemplate="{StaticResource elementFactory}" />
</ScrollViewer>
</UserControl>

View File

@ -0,0 +1,104 @@
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using HangoverAvalonia.ViewModels;
using ReactiveUI;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace HangoverAvalonia.Controls
{
public partial class CheckedListBox : UserControl
{
public event EventHandler<ItemCheckEventArgs> ItemCheck;
public static readonly StyledProperty<IEnumerable> ItemsProperty =
AvaloniaProperty.Register<CheckedListBox, IEnumerable>(nameof(Items));
public IEnumerable Items { get => GetValue(ItemsProperty); set => SetValue(ItemsProperty, value); }
private CheckedListBoxViewModel _viewModel = new();
public IEnumerable<object> CheckedItems =>
_viewModel
.CheckboxItems
.Where(i => i.IsChecked)
.Select(i => i.Item);
public void SetItemChecked(int i, bool isChecked) => _viewModel.CheckboxItems[i].IsChecked = isChecked;
public void SetItemChecked(object item, bool isChecked)
{
var obj = _viewModel.CheckboxItems.SingleOrDefault(i => i.Item == item);
if (obj is not null)
obj.IsChecked = isChecked;
}
public CheckedListBox()
{
InitializeComponent();
scroller.DataContext = _viewModel;
_viewModel.CheckedChanged += _viewModel_CheckedChanged;
}
private void _viewModel_CheckedChanged(object sender, CheckBoxViewModel e)
{
var args = new ItemCheckEventArgs { Item = e.Item, ItemIndex = _viewModel.CheckboxItems.IndexOf(e), IsChecked = e.IsChecked };
ItemCheck?.Invoke(this, args);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (change.Property.Name == nameof(Items) && Items != null)
_viewModel.SetItems(Items);
base.OnPropertyChanged(change);
}
public class CheckedListBoxViewModel : ViewModelBase
{
public event EventHandler<CheckBoxViewModel> CheckedChanged;
public AvaloniaList<CheckBoxViewModel> CheckboxItems { get; private set; }
public void SetItems(IEnumerable items)
{
UnsubscribeFromItems(CheckboxItems);
CheckboxItems = new(items.OfType<object>().Select(o => new CheckBoxViewModel { Item = o }));
SubscribeToItems(CheckboxItems);
this.RaisePropertyChanged(nameof(CheckboxItems));
}
private void SubscribeToItems(IEnumerable objects)
{
foreach (var i in objects.OfType<INotifyPropertyChanged>())
i.PropertyChanged += I_PropertyChanged;
}
private void UnsubscribeFromItems(AvaloniaList<CheckBoxViewModel> objects)
{
if (objects is null) return;
foreach (var i in objects)
i.PropertyChanged -= I_PropertyChanged;
}
private void I_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
CheckedChanged?.Invoke(this, (CheckBoxViewModel)sender);
}
}
public class CheckBoxViewModel : ViewModelBase
{
private bool _isChecked;
public bool IsChecked { get => _isChecked; set => this.RaiseAndSetIfChanged(ref _isChecked, value); }
private object _bookText;
public object Item { get => _bookText; set => this.RaiseAndSetIfChanged(ref _bookText, value); }
}
}
public class ItemCheckEventArgs : EventArgs
{
public int ItemIndex { get; init; }
public bool IsChecked { get; init; }
public object Item { get; init; }
}
}

View File

@ -51,6 +51,11 @@
<None Remove="hangover.ico" />
</ItemGroup>
<ItemGroup>
<Compile Update="ViewModels\MainVM.*.cs">
<DependentUpon>MainVM.cs</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="hangover.ico" />

View File

@ -1,5 +1,4 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.ReactiveUI;
using System;

View File

@ -0,0 +1,36 @@
using HangoverBase;
using ReactiveUI;
namespace HangoverAvalonia.ViewModels
{
public partial class MainVM
{
private DatabaseTab _tab;
private string _databaseFileText;
private bool _databaseFound;
private string _sqlResults;
public string DatabaseFileText { get => _databaseFileText; set => this.RaiseAndSetIfChanged(ref _databaseFileText, value); }
public string SqlQuery { get; set; }
public bool DatabaseFound { get => _databaseFound; set => this.RaiseAndSetIfChanged(ref _databaseFound, value); }
public string SqlResults { get => _sqlResults; set => this.RaiseAndSetIfChanged(ref _sqlResults, value); }
private void Load_databaseVM()
{
_tab = new(new(() => SqlQuery, s => SqlResults = s, s => SqlResults = s));
_tab.LoadDatabaseFile();
if (_tab.DbFile is null)
{
DatabaseFileText = $"Database file not found";
DatabaseFound = false;
return;
}
DatabaseFileText = $"Database file: {_tab.DbFile}";
DatabaseFound = true;
}
public void ExecuteQuery() => _tab.ExecuteQuery();
}
}

View File

@ -0,0 +1,41 @@
using ApplicationServices;
using DataLayer;
using ReactiveUI;
using System.Collections.Generic;
namespace HangoverAvalonia.ViewModels
{
public partial class MainVM
{
private List<LibraryBook> _deletedBooks;
public List<LibraryBook> DeletedBooks { get => _deletedBooks; set => this.RaiseAndSetIfChanged(ref _deletedBooks, value); }
public string CheckedCountText => $"Checked : {_checkedBooksCount} of {_totalBooksCount}";
private int _totalBooksCount = 0;
private int _checkedBooksCount = 0;
public int CheckedBooksCount
{
get => _checkedBooksCount;
set
{
if (_checkedBooksCount != value)
{
_checkedBooksCount = value;
this.RaisePropertyChanged(nameof(CheckedCountText));
}
}
}
private void Load_deletedVM()
{
reload();
}
public void reload()
{
DeletedBooks = DbContexts.GetContext().GetDeletedLibraryBooks();
_checkedBooksCount = 0;
_totalBooksCount = DeletedBooks.Count;
this.RaisePropertyChanged(nameof(CheckedCountText));
}
}
}

View File

@ -0,0 +1,11 @@
namespace HangoverAvalonia.ViewModels
{
public partial class MainVM : ViewModelBase
{
public MainVM()
{
Load_databaseVM();
Load_deletedVM();
}
}
}

View File

@ -1,37 +0,0 @@
using System;
using HangoverBase;
using ReactiveUI;
namespace HangoverAvalonia.ViewModels
{
public class MainWindowViewModel : ViewModelBase
{
private DatabaseTab _tab;
private string _databaseFileText;
private bool _databaseFound;
private string _sqlResults;
public string DatabaseFileText { get => _databaseFileText; set => this.RaiseAndSetIfChanged(ref _databaseFileText, value); }
public string SqlQuery { get; set; }
public bool DatabaseFound { get => _databaseFound; set => this.RaiseAndSetIfChanged(ref _databaseFound, value); }
public string SqlResults { get => _sqlResults; set => this.RaiseAndSetIfChanged(ref _sqlResults, value); }
public MainWindowViewModel()
{
_tab = new(new(() => SqlQuery, s => SqlResults = s, s => SqlResults = s));
_tab.LoadDatabaseFile();
if (_tab.DbFile is null)
{
DatabaseFileText = $"Database file not found";
DatabaseFound = false;
return;
}
DatabaseFileText = $"Database file: {_tab.DbFile}";
DatabaseFound = true;
}
public void ExecuteQuery() => _tab.ExecuteQuery();
}
}

View File

@ -1,7 +1,4 @@
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Text;
namespace HangoverAvalonia.ViewModels
{

View File

@ -0,0 +1,11 @@
namespace HangoverAvalonia.Views
{
public partial class MainWindow
{
private void cliTab_VisibleChanged(bool isVisible)
{
if (!isVisible)
return;
}
}
}

View File

@ -0,0 +1,16 @@
namespace HangoverAvalonia.Views
{
public partial class MainWindow
{
private void databaseTab_VisibleChanged(bool isVisible)
{
if (!isVisible)
return;
}
public void Execute_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_viewModel.ExecuteQuery();
}
}
}

View File

@ -0,0 +1,40 @@
using ApplicationServices;
using DataLayer;
using HangoverAvalonia.Controls;
using System.Linq;
namespace HangoverAvalonia.Views
{
public partial class MainWindow
{
private void deletedTab_VisibleChanged(bool isVisible)
{
if (!isVisible)
return;
if (_viewModel.DeletedBooks.Count == 0)
_viewModel.reload();
}
public void Deleted_CheckedListBox_ItemCheck(object sender, ItemCheckEventArgs args)
{
_viewModel.CheckedBooksCount = deletedCbl.CheckedItems.Count();
}
public void Deleted_CheckAll_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
foreach (var item in deletedCbl.Items)
deletedCbl.SetItemChecked(item, true);
}
public void Deleted_UncheckAll_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
foreach (var item in deletedCbl.Items)
deletedCbl.SetItemChecked(item, false);
}
public void Deleted_Save_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var libraryBooksToRestore = deletedCbl.CheckedItems.Cast<LibraryBook>().ToList();
var qtyChanges = libraryBooksToRestore.RestoreBooks();
if (qtyChanges > 0)
_viewModel.reload();
}
}
}

View File

@ -3,33 +3,36 @@
xmlns:vm="using:HangoverAvalonia.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:HangoverAvalonia.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="500"
Width="800" Height="500"
x:Class="HangoverAvalonia.Views.MainWindow"
Icon="/Assets/hangover.ico "
Title="Hangover: Libation debug and recovery tool">
<Design.DataContext>
<vm:MainWindowViewModel/>
<vm:MainVM/>
</Design.DataContext>
<TabControl Grid.Row="0">
<TabControl Name="tabControl1" Grid.Row="0">
<TabControl.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="18"/>
<Setter Property="Height" Value="23"/>
</Style>
<Style Selector="TabItem">
<Setter Property="MinHeight" Value="30"/>
<Setter Property="Height" Value="30"/>
<Setter Property="Padding" Value="8,2,8,0"/>
<Setter Property="MinHeight" Value="40"/>
<Setter Property="Height" Value="40"/>
<Setter Property="Padding" Value="8,2,8,5"/>
</Style>
<Style Selector="TabItem#Header TextBlock">
<Setter Property="MinHeight" Value="5"/>
</Style>
<Style Selector="Button">
<Setter Property="Padding" Value="20,5,20,5"/>
</Style>
</TabControl.Styles>
<!-- Database Tab -->
<TabItem>
<TabItem Name="databaseTab">
<TabItem.Header>
<TextBlock FontSize="14" VerticalAlignment="Center">Database</TextBlock>
</TabItem.Header>
@ -52,7 +55,6 @@
<Button
Grid.Row="3"
Padding="20,5,20,5"
Content="Execute"
IsEnabled="{Binding DatabaseFound}"
Click="Execute_Click" />
@ -65,8 +67,45 @@
</Grid>
</TabItem>
<!-- Deleted Books Tab -->
<TabItem Name="deletedTab">
<TabItem.Header>
<TextBlock FontSize="14" VerticalAlignment="Center">Deleted Books</TextBlock>
</TabItem.Header>
<Grid
RowDefinitions="Auto,*,Auto">
<TextBlock
Grid.Row="0"
Margin="5"
Text="To restore deleted book, check box and save" />
<controls:CheckedListBox
Grid.Row="1"
Margin="5,0,5,0"
BorderThickness="1"
BorderBrush="Gray"
Name="deletedCbl"
Items="{Binding DeletedBooks}" />
<Grid
Grid.Row="2"
Margin="5"
ColumnDefinitions="Auto,Auto,Auto,*">
<Button Grid.Column="0" Margin="0,0,20,0" Content="Check All" Click="Deleted_CheckAll_Click" />
<Button Grid.Column="1" Margin="0,0,20,0" Content="Uncheck All" Click="Deleted_UncheckAll_Click" />
<TextBlock Grid.Column="2" VerticalAlignment="Center" Text="{Binding CheckedCountText}" />
<Button Grid.Column="3" HorizontalAlignment="Right" Content="Save" Click="Deleted_Save_Click" />
</Grid>
</Grid>
</TabItem>
<!-- Command Line Interface Tab -->
<TabItem>
<TabItem Name="cliTab">
<TabItem.Header>
<TextBlock FontSize="14" VerticalAlignment="Center">Command Line Interface</TextBlock>
</TabItem.Header>

View File

@ -6,7 +6,7 @@ namespace HangoverAvalonia.Views
{
public partial class MainWindow : Window
{
MainWindowViewModel _viewModel => DataContext as MainWindowViewModel;
MainVM _viewModel => DataContext as MainVM;
public MainWindow()
{
InitializeComponent();
@ -16,9 +16,12 @@ namespace HangoverAvalonia.Views
LibationScaffolding.RunPostMigrationScaffolding(config);
}
public void Execute_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
public void OnLoad()
{
_viewModel.ExecuteQuery();
deletedCbl.ItemCheck += Deleted_CheckedListBox_ItemCheck;
databaseTab.PropertyChanged += (_, e) => { if (e.Property.Name == nameof(TabItem.IsSelected)) databaseTab_VisibleChanged(databaseTab.IsSelected); };
deletedTab.PropertyChanged += (_, e) => { if (e.Property.Name == nameof(TabItem.IsSelected)) deletedTab_VisibleChanged(deletedTab.IsSelected); };
cliTab.PropertyChanged += (_, e) => { if (e.Property.Name == nameof(TabItem.IsSelected)) cliTab_VisibleChanged(cliTab.IsSelected); };
}
}
}

View File

@ -143,7 +143,7 @@ namespace LibationAvalonia.Dialogs
get => _replacementText;
set
{
if (ReplacementCharacters.ContainsInvalidPathChar(value))
if (ReplacementCharacters.ContainsInvalidFilenameChar(value))
this.RaisePropertyChanged(nameof(ReplacementText));
else
this.RaiseAndSetIfChanged(ref _replacementText, value);
@ -158,7 +158,7 @@ namespace LibationAvalonia.Dialogs
set
{
if (value?.Length != 1 || !ReplacementCharacters.ContainsInvalidPathChar(value))
if (value?.Length != 1)
this.RaisePropertyChanged(nameof(CharacterToReplace));
else
{

View File

@ -1,4 +1,5 @@
using System;
using Avalonia.Threading;
using System;
using System.Threading.Tasks;
namespace LibationAvalonia
@ -40,19 +41,15 @@ namespace LibationAvalonia
private static async void LogMe_LogError(object sender, (Exception, string) tuple)
{
await Task.Run(() => LogForm?.WriteLine(tuple.Item2 ?? "Automated backup: error"));
await Task.Run(() => LogForm?.WriteLine("ERROR: " + tuple.Item1.Message));
await Dispatcher.UIThread.InvokeAsync(() => LogForm?.WriteLine(tuple.Item2 ?? "Automated backup: error"));
await Dispatcher.UIThread.InvokeAsync(() => LogForm?.WriteLine("ERROR: " + tuple.Item1.Message));
}
private static async void LogMe_LogErrorString(object sender, string text)
{
await Task.Run(() => LogForm?.WriteLine(text));
}
=> await Dispatcher.UIThread.InvokeAsync(() => LogForm?.WriteLine(text));
private static async void LogMe_LogInfo(object sender, string text)
{
await Task.Run(() => LogForm?.WriteLine(text));
}
=> await Dispatcher.UIThread.InvokeAsync(() => LogForm?.WriteLine(text));
public void Info(string text) => LogInfo?.Invoke(this, text);
public void Error(string text) => LogErrorString?.Invoke(this, text);

View File

@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ApplicationServices;
using AudibleApi;
using AudibleApi.Common;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using DataLayer;
@ -22,7 +24,9 @@ namespace LibationAvalonia.ViewModels
ValidationFail,
FailedRetry,
FailedSkip,
FailedAbort
FailedAbort,
LicenseDenied,
LicenseDeniedPossibleOutage
}
public enum ProcessBookStatus
@ -80,6 +84,8 @@ namespace LibationAvalonia.ViewModels
ProcessBookResult.FailedRetry => "Error, will retry later",
ProcessBookResult.FailedSkip => "Error, Skippping",
ProcessBookResult.FailedAbort => "Error, Abort",
ProcessBookResult.LicenseDenied => "License Denied",
ProcessBookResult.LicenseDeniedPossibleOutage => "Possible Service Interruption",
_ => Status.ToString(),
};
@ -134,18 +140,31 @@ namespace LibationAvalonia.ViewModels
return Result = ProcessBookResult.Success;
else if (statusHandler.Errors.Contains("Cancelled"))
{
Logger.Info($"{procName}: Process was cancelled {LibraryBook.Book}");
Logger.Info($"{procName}: Process was cancelled - {LibraryBook.Book}");
return Result = ProcessBookResult.Cancelled;
}
else if (statusHandler.Errors.Contains("Validation failed"))
{
Logger.Info($"{procName}: Validation failed {LibraryBook.Book}");
Logger.Info($"{procName}: Validation failed - {LibraryBook.Book}");
return Result = ProcessBookResult.ValidationFail;
}
foreach (var errorMessage in statusHandler.Errors)
Logger.Error($"{procName}: {errorMessage}");
}
catch (ContentLicenseDeniedException ldex)
{
if (ldex.AYCL?.RejectionReason is null or RejectionReason.GenericError)
{
Logger.Info($"{procName}: Content license was denied, but this error appears to be caused by a temporary interruption of service. - {LibraryBook.Book}");
return Result = ProcessBookResult.LicenseDeniedPossibleOutage;
}
else
{
Logger.Info($"{procName}: Content license denied. Check your Audible account to see if you have access to this title. - {LibraryBook.Book}");
return Result = ProcessBookResult.LicenseDenied;
}
}
catch (Exception ex)
{
Logger.Error(ex, procName);

View File

@ -163,6 +163,8 @@ namespace LibationAvalonia.ViewModels
using var counterTimer = new System.Threading.Timer(CounterTimer_Tick, null, 0, 500);
bool shownServiceOutageMessage = false;
while (Queue.MoveNext())
{
var nextBook = Queue.Current;
@ -178,7 +180,19 @@ namespace LibationAvalonia.ViewModels
else if (result == ProcessBookResult.FailedAbort)
Queue.ClearQueue();
else if (result == ProcessBookResult.FailedSkip)
nextBook.LibraryBook.Book.UpdateBookStatus(DataLayer.LiberatedStatus.Error);
nextBook.LibraryBook.Book.UpdateBookStatus(LiberatedStatus.Error);
else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage)
{
await MessageBox.Show(@$"
You were denied a content license for {nextBook.LibraryBook.Book.Title}
This error appears to be caused by a temporary interruption of service that sometimes affects Libation's users. This type of error usually resolves itself in 1 to 2 days, and in the meantime you should still be able to access your books through Audible's website or app.
",
"Possible Interruption of Service",
MessageBoxButtons.OK,
MessageBoxIcon.Asterisk);
shownServiceOutageMessage = true;
}
}
Serilog.Log.Logger.Information("Completed processing queue");

View File

@ -4,6 +4,7 @@ using System.ComponentModel;
using System.Linq;
using Dinah.Core;
using Dinah.Core.Logging;
using FileManager;
using Microsoft.Extensions.Configuration;
using Serilog;
using Serilog.Events;
@ -21,6 +22,7 @@ namespace LibationFileManager
.Build();
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.Destructure.ByTransforming<LongPath>(lp => lp.Path)
.CreateLogger();
}

View File

@ -276,7 +276,7 @@ namespace LibationFileManager
#region templates: custom file naming
[Description("Edit how illegal filename characters are replaced")]
[Description("Edit how filename characters are replaced")]
public ReplacementCharacters ReplacementCharacters
{
get => persistentDictionary.GetNonString<ReplacementCharacters>(nameof(ReplacementCharacters));

View File

@ -101,7 +101,7 @@ namespace LibationWinForms.Dialogs
{
dataGridView1.Rows[e.RowIndex].ErrorText = $"The {charToReplaceStr[0]} character is already being replaced";
}
else if (ReplacementCharacters.ContainsInvalidPathChar(replacement))
else if (ReplacementCharacters.ContainsInvalidFilenameChar(replacement))
{
dataGridView1.Rows[e.RowIndex].ErrorText = $"Your {replacementStringCol.HeaderText} contains illegal characters";
}

View File

@ -535,7 +535,7 @@
this.editCharreplacementBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.editCharreplacementBtn.Location = new System.Drawing.Point(5, 158);
this.editCharreplacementBtn.Name = "editCharreplacementBtn";
this.editCharreplacementBtn.Size = new System.Drawing.Size(387, 23);
this.editCharreplacementBtn.Size = new System.Drawing.Size(281, 23);
this.editCharreplacementBtn.TabIndex = 8;
this.editCharreplacementBtn.Text = "[edit char replacement desc]";
this.editCharreplacementBtn.UseVisualStyleBackColor = true;

View File

@ -7,6 +7,8 @@ using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows.Forms;
using ApplicationServices;
using AudibleApi.Common;
using AudibleApi;
using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
@ -24,7 +26,9 @@ namespace LibationWinForms.ProcessQueue
ValidationFail,
FailedRetry,
FailedSkip,
FailedAbort
FailedAbort,
LicenseDenied,
LicenseDeniedPossibleOutage
}
public enum ProcessBookStatus
@ -108,18 +112,31 @@ namespace LibationWinForms.ProcessQueue
return Result = ProcessBookResult.Success;
else if (statusHandler.Errors.Contains("Cancelled"))
{
Logger.Info($"{procName}: Process was cancelled {LibraryBook.Book}");
Logger.Info($"{procName}: Process was cancelled - {LibraryBook.Book}");
return Result = ProcessBookResult.Cancelled;
}
else if (statusHandler.Errors.Contains("Validation failed"))
{
Logger.Info($"{procName}: Validation failed {LibraryBook.Book}");
Logger.Info($"{procName}: Validation failed - {LibraryBook.Book}");
return Result = ProcessBookResult.ValidationFail;
}
foreach (var errorMessage in statusHandler.Errors)
Logger.Error($"{procName}: {errorMessage}");
}
catch (ContentLicenseDeniedException ldex)
{
if (ldex.AYCL?.RejectionReason is null or RejectionReason.GenericError)
{
Logger.Info($"{procName}: Content license was denied, but this error appears to be caused by a temporary interruption of service. - {LibraryBook.Book}");
return Result = ProcessBookResult.LicenseDeniedPossibleOutage;
}
else
{
Logger.Info($"{procName}: Content license denied. Check your Audible account to see if you have access to this title. - {LibraryBook.Book}");
return Result = ProcessBookResult.LicenseDenied;
}
}
catch (Exception ex)
{
Logger.Error(ex, procName);

View File

@ -60,68 +60,34 @@ namespace LibationWinForms.ProcessQueue
public void SetResult(ProcessBookResult result)
{
string statusText = default;
switch (result)
(string statusText, ProcessBookStatus status) = result switch
{
case ProcessBookResult.Success:
statusText = "Finished";
Status = ProcessBookStatus.Completed;
break;
case ProcessBookResult.Cancelled:
statusText = "Cancelled";
Status = ProcessBookStatus.Cancelled;
break;
case ProcessBookResult.FailedRetry:
statusText = "Error, will retry later";
Status = ProcessBookStatus.Failed;
break;
case ProcessBookResult.FailedSkip:
statusText = "Error, Skippping";
Status = ProcessBookStatus.Failed;
break;
case ProcessBookResult.FailedAbort:
statusText = "Error, Abort";
Status = ProcessBookStatus.Failed;
break;
case ProcessBookResult.ValidationFail:
statusText = "Validion fail";
Status = ProcessBookStatus.Failed;
break;
case ProcessBookResult.None:
statusText = "UNKNOWN";
Status = ProcessBookStatus.Failed;
break;
}
ProcessBookResult.Success => ("Finished", ProcessBookStatus.Completed),
ProcessBookResult.Cancelled => ("Cancelled", ProcessBookStatus.Cancelled),
ProcessBookResult.FailedRetry => ("Error, will retry later", ProcessBookStatus.Failed),
ProcessBookResult.FailedSkip => ("Error, Skippping", ProcessBookStatus.Failed),
ProcessBookResult.FailedAbort => ("Error, Abort", ProcessBookStatus.Failed),
ProcessBookResult.ValidationFail => ("Validion fail", ProcessBookStatus.Failed),
ProcessBookResult.LicenseDenied => ("License Denied", ProcessBookStatus.Failed),
ProcessBookResult.LicenseDeniedPossibleOutage => ("Possible Service Interruption", ProcessBookStatus.Failed),
_ => ("UNKNOWN", ProcessBookStatus.Failed),
};
SetStatus(Status, statusText);
SetStatus(status, statusText);
}
public void SetStatus(ProcessBookStatus status, string statusText = null)
{
Color backColor = default;
switch (status)
Status = status;
Color backColor = Status switch
{
case ProcessBookStatus.Completed:
backColor = SuccessColor;
Status = ProcessBookStatus.Completed;
break;
case ProcessBookStatus.Cancelled:
backColor = CancelledColor;
Status = ProcessBookStatus.Cancelled;
break;
case ProcessBookStatus.Queued:
backColor = QueuedColor;
Status = ProcessBookStatus.Queued;
break;
case ProcessBookStatus.Working:
backColor = QueuedColor;
Status = ProcessBookStatus.Working;
break;
case ProcessBookStatus.Failed:
backColor = FailedColor;
Status = ProcessBookStatus.Failed;
break;
}
ProcessBookStatus.Completed => SuccessColor,
ProcessBookStatus.Cancelled => CancelledColor,
ProcessBookStatus.Queued => QueuedColor,
ProcessBookStatus.Working => QueuedColor,
_ => FailedColor
};
SuspendLayout();

View File

@ -161,6 +161,8 @@ namespace LibationWinForms.ProcessQueue
StartingTime = DateTime.Now;
counterTimer.Start();
bool shownServiceOutageMessage = false;
while (Queue.MoveNext())
{
var nextBook = Queue.Current;
@ -177,6 +179,18 @@ namespace LibationWinForms.ProcessQueue
Queue.ClearQueue();
else if (result == ProcessBookResult.FailedSkip)
nextBook.LibraryBook.Book.UpdateBookStatus(DataLayer.LiberatedStatus.Error);
else if (result == ProcessBookResult.LicenseDeniedPossibleOutage && !shownServiceOutageMessage)
{
MessageBox.Show(@$"
You were denied a content license for {nextBook.LibraryBook.Book.Title}
This error appears to be caused by a temporary interruption of service that sometimes affects Libation's users. This type of error usually resolves itself in 1 to 2 days, and in the meantime you should still be able to access your books through Audible's website or app.
",
"Possible Interruption of Service",
MessageBoxButtons.OK,
MessageBoxIcon.Asterisk);
shownServiceOutageMessage = true;
}
}
Serilog.Log.Logger.Information("Completed processing queue");

View File

@ -15,16 +15,18 @@ namespace FileNamingTemplateTests
static ReplacementCharacters Replacements = ReplacementCharacters.Default;
[TestMethod]
public void equiv_GetValidFilename()
[DataRow(@"C:\foo\bar", @"C:\foo\bar\my book 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 [ID123456].txt", PlatformID.Win32NT)]
[DataRow(@"/foo/bar", @"/foo/bar/my: book 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 [ID123456].txt", PlatformID.Unix)]
public void equiv_GetValidFilename(string dirFullPath, string expected, PlatformID platformID)
{
if (Environment.OSVersion.Platform != platformID)
return;
var sb = new System.Text.StringBuilder();
sb.Append('0', 300);
var longText = sb.ToString();
var expectedNew = @"C:\foo\bar\my book 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 [ID123456].txt";
var f2 = NEW_GetValidFilename_FileNamingTemplate(@"C:\foo\bar", "my: book " + longText, "txt", "ID123456");
f2.Should().Be(expectedNew);
NEW_GetValidFilename_FileNamingTemplate(dirFullPath, "my: book " + longText, "txt", "ID123456").Should().Be(expected);
}
private static string NEW_GetValidFilename_FileNamingTemplate(string dirFullPath, string filename, string extension, string metadataSuffix)
@ -40,12 +42,12 @@ namespace FileNamingTemplateTests
}
[TestMethod]
public void equiv_GetMultipartFileName()
[DataRow(@"C:\foo\bar\my file.txt", @"C:\foo\bar\my file - 002 - title.txt", PlatformID.Win32NT)]
[DataRow(@"/foo/bar/my file.txt", @"/foo/bar/my file - 002 - title.txt", PlatformID.Unix)]
public void equiv_GetMultipartFileName(string inStr, string outStr, PlatformID platformID)
{
var expected = @"C:\foo\bar\my file - 002 - title.txt";
var f2 = NEW_GetMultipartFileName_FileNamingTemplate(@"C:\foo\bar\my file.txt", 2, 100, "title");
f2.Should().Be(expected);
if (Environment.OSVersion.Platform == platformID)
NEW_GetMultipartFileName_FileNamingTemplate(inStr, 2, 100, "title").Should().Be(outStr);
}
private static string NEW_GetMultipartFileName_FileNamingTemplate(string originalPath, int partsPosition, int partsTotal, string suffix)
@ -64,11 +66,16 @@ namespace FileNamingTemplateTests
}
[TestMethod]
public void remove_slashes()
[DataRow(@"\foo\<title>.txt", @"\foo\slashes.txt", PlatformID.Win32NT)]
[DataRow(@"/foo/<title>.txt", @"/foo/s\la\sh\es.txt", PlatformID.Unix)]
public void remove_slashes(string inStr, string outStr, PlatformID platformID)
{
var fileNamingTemplate = new FileNamingTemplate(@"\foo\<title>.txt");
if (Environment.OSVersion.Platform == platformID)
{
var fileNamingTemplate = new FileNamingTemplate(inStr);
fileNamingTemplate.AddParameterReplacement("title", @"s\l/a\s/h\e/s");
fileNamingTemplate.GetFilePath(Replacements).PathWithoutPrefix.Should().Be(@"\foo\slashes.txt");
fileNamingTemplate.GetFilePath(Replacements).PathWithoutPrefix.Should().Be(outStr);
}
}
}
}

View File

@ -21,51 +21,78 @@ namespace FileUtilityTests
[TestMethod]
// non-empty replacement
[DataRow("abc*abc.txt", "abc✱abc.txt")]
// standardize slashes
[DataRow(@"a/b\c/d", @"a\b\c\d")]
[DataRow("abc*abc.txt", "abc✱abc.txt", PlatformID.Win32NT)]
[DataRow("abc*abc.txt", "abc*abc.txt", PlatformID.Unix)]
// standardize slashes. There is no unix equivalent because there is no alt directory separator
[DataRow(@"a/b\c/d", @"a\b\c\d", PlatformID.Win32NT)]
// remove illegal chars
[DataRow("a*?:z.txt", "a✱z.txt")]
[DataRow("a*?:z.txt", "a✱z.txt", PlatformID.Win32NT)]
[DataRow("a*?:z.txt", "a*?:z.txt", PlatformID.Unix)]
// retain drive letter path colon
[DataRow(@"C:\az.txt", @"C:\az.txt")]
[DataRow(@"C:\az.txt", @"C:\az.txt", PlatformID.Win32NT)]
[DataRow(@"/:/az.txt", @"/:/az.txt", PlatformID.Unix)]
// replace all other colons
[DataRow(@"a\b:c\d.txt", @"a\bc\d.txt")]
[DataRow(@"a\b:c\d.txt", @"a\bc\d.txt", PlatformID.Win32NT)]
[DataRow(@"a/b:c/d.txt", @"a/b:c/d.txt", PlatformID.Unix)]
// remove empty directories
[DataRow(@"C:\a\\\b\c\\\d.txt", @"C:\a\b\c\d.txt")]
[DataRow(@"C:\""foo\<id>", @"C:\“foo\id")]
public void DefaultTests(string inStr, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, Default).PathWithoutPrefix);
[DataRow(@"C:\a\\\b\c\\\d.txt", @"C:\a\b\c\d.txt", PlatformID.Win32NT)]
[DataRow(@"/a///b/c///d.txt", @"/a/b/c/d.txt", PlatformID.Unix)]
[DataRow(@"C:\""foo\<id>", @"C:\“foo\id", PlatformID.Win32NT)]
[DataRow(@"/""foo/<id>", @"/“foo/<id>", PlatformID.Unix)]
public void DefaultTests(string inStr, string outStr, PlatformID platformID)
=> Test(inStr, outStr, Default, platformID);
[TestMethod]
// non-empty replacement
[DataRow("abc*abc.txt", "abc_abc.txt")]
// standardize slashes
[DataRow(@"a/b\c/d", @"a\b\c\d")]
[DataRow("abc*abc.txt", "abc_abc.txt", PlatformID.Win32NT)]
[DataRow("abc*abc.txt", "abc*abc.txt", PlatformID.Unix)]
// standardize slashes. There is no unix equivalent because there is no alt directory separator
[DataRow(@"a/b\c/d", @"a\b\c\d", PlatformID.Win32NT)]
// remove illegal chars
[DataRow("a*?:z.txt", "a__-z.txt")]
[DataRow("a*?:z.txt", "a__-z.txt", PlatformID.Win32NT)]
[DataRow("a*?:z.txt", "a*?:z.txt", PlatformID.Unix)]
// retain drive letter path colon
[DataRow(@"C:\az.txt", @"C:\az.txt")]
[DataRow(@"C:\az.txt", @"C:\az.txt", PlatformID.Win32NT)]
[DataRow(@"/:az.txt", @"/:az.txt", PlatformID.Unix)]
// replace all other colons
[DataRow(@"a\b:c\d.txt", @"a\b-c\d.txt")]
[DataRow(@"a\b:c\d.txt", @"a\b-c\d.txt", PlatformID.Win32NT)]
[DataRow(@"a/b:c/d.txt", @"a/b:c/d.txt", PlatformID.Unix)]
// remove empty directories
[DataRow(@"C:\a\\\b\c\\\d.txt", @"C:\a\b\c\d.txt")]
[DataRow(@"C:\""foo\<id>", @"C:\'foo\{id}")]
public void LoFiDefaultTests(string inStr, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, LoFiDefault).PathWithoutPrefix);
[DataRow(@"C:\a\\\b\c\\\d.txt", @"C:\a\b\c\d.txt", PlatformID.Win32NT)]
[DataRow(@"/a///b/c///d.txt", @"/a/b/c/d.txt", PlatformID.Unix)]
[DataRow(@"C:\""foo\<id>", @"C:\'foo\{id}", PlatformID.Win32NT)]
[DataRow(@"/""foo/<id>", @"/""foo/<id>", PlatformID.Unix)]
public void LoFiDefaultTests(string inStr, string outStr, PlatformID platformID)
=> Test(inStr, outStr, LoFiDefault, platformID);
[TestMethod]
// empty replacement
[DataRow("abc*abc.txt", "abc_abc.txt")]
// standardize slashes
[DataRow(@"a/b\c/d", @"a\b\c\d")]
[DataRow("abc*abc.txt", "abc_abc.txt", PlatformID.Win32NT)]
[DataRow("abc*abc.txt", "abc*abc.txt", PlatformID.Unix)]
// standardize slashes. There is no unix equivalent because there is no alt directory separator
[DataRow(@"a/b\c/d", @"a\b\c\d", PlatformID.Win32NT)]
// remove illegal chars
[DataRow("a*?:z.txt", "a___z.txt")]
[DataRow("a*?:z.txt", "a___z.txt", PlatformID.Win32NT)]
[DataRow("a*?:z.txt", "a*?:z.txt", PlatformID.Unix)]
// retain drive letter path colon
[DataRow(@"C:\az.txt", @"C:\az.txt")]
[DataRow(@"C:\az.txt", @"C:\az.txt", PlatformID.Win32NT)]
[DataRow(@"/:az.txt", @"/:az.txt", PlatformID.Unix)]
// replace all other colons
[DataRow(@"a\b:c\d.txt", @"a\b_c\d.txt")]
[DataRow(@"a\b:c\d.txt", @"a\b_c\d.txt", PlatformID.Win32NT)]
[DataRow(@"a/b:c/d.txt", @"a/b:c/d.txt", PlatformID.Unix)]
// remove empty directories
[DataRow(@"C:\a\\\b\c\\\d.txt", @"C:\a\b\c\d.txt")]
[DataRow(@"C:\""foo\<id>", @"C:\_foo\_id_")]
public void BarebonesDefaultTests(string inStr, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, Barebones).PathWithoutPrefix);
[DataRow(@"C:\a\\\b\c\\\d.txt", @"C:\a\b\c\d.txt", PlatformID.Win32NT)]
[DataRow(@"/a///b/c///d.txt", @"/a/b/c/d.txt", PlatformID.Unix)]
[DataRow(@"C:\""foo\<id>", @"C:\_foo\_id_", PlatformID.Win32NT)]
[DataRow(@"/""foo/<id>", @"/""foo/<id>", PlatformID.Unix)]
public void BarebonesDefaultTests(string inStr, string outStr, PlatformID platformID)
=> Test(inStr, outStr, Barebones, platformID);
private void Test(string inStr, string outStr, ReplacementCharacters replacements, PlatformID platformID)
{
if (Environment.OSVersion.Platform == platformID)
FileUtility.GetSafePath(inStr, replacements).PathWithoutPrefix.Should().Be(outStr);
}
}
[TestClass]
@ -77,23 +104,33 @@ namespace FileUtilityTests
// needs separate method. middle null param not running correctly in TestExplorer when used in DataRow()
[TestMethod]
[DataRow("http://test.com/a/b/c", "httptest.comabc")]
public void url_null_replacement(string inStr, string outStr) => DefaultReplacementTest(inStr, outStr);
[DataRow("http://test.com/a/b/c", "httptest.comabc", PlatformID.Win32NT)]
[DataRow("http://test.com/a/b/c", "http:test.comabc", PlatformID.Unix)]
public void url_null_replacement(string inStr, string outStr, PlatformID platformID) => DefaultReplacementTest(inStr, outStr, platformID);
[TestMethod]
// empty replacement
[DataRow("http://test.com/a/b/c", "httptest.comabc")]
public void DefaultReplacementTest(string inStr, string outStr) => Default.ReplaceInvalidFilenameChars(inStr).Should().Be(outStr);
[DataRow("http://test.com/a/b/c", "httptest.comabc", PlatformID.Win32NT)]
[DataRow("http://test.com/a/b/c", "http:test.comabc", PlatformID.Unix)]
public void DefaultReplacementTest(string inStr, string outStr, PlatformID platformID) => Test(inStr, outStr, Default, platformID);
[TestMethod]
// empty replacement
[DataRow("http://test.com/a/b/c", "http-__test.com_a_b_c")]
public void LoFiDefaultReplacementTest(string inStr, string outStr) => LoFiDefault.ReplaceInvalidFilenameChars(inStr).Should().Be(outStr);
[DataRow("http://test.com/a/b/c", "http-__test.com_a_b_c", PlatformID.Win32NT)]
[DataRow("http://test.com/a/b/c", "http:__test.com_a_b_c", PlatformID.Unix)]
public void LoFiDefaultReplacementTest(string inStr, string outStr, PlatformID platformID) => Test(inStr, outStr, LoFiDefault, platformID);
[TestMethod]
// empty replacement
[DataRow("http://test.com/a/b/c", "http___test.com_a_b_c")]
public void BarebonesDefaultReplacementTest(string inStr, string outStr) => Barebones.ReplaceInvalidFilenameChars(inStr).Should().Be(outStr);
[DataRow("http://test.com/a/b/c", "http___test.com_a_b_c", PlatformID.Win32NT)]
[DataRow("http://test.com/a/b/c", "http:__test.com_a_b_c", PlatformID.Unix)]
public void BarebonesDefaultReplacementTest(string inStr, string outStr, PlatformID platformID) => Test(inStr, outStr, Barebones, platformID);
private void Test(string inStr, string outStr, ReplacementCharacters replacements, PlatformID platformID)
{
if (Environment.OSVersion.Platform == platformID)
replacements.ReplaceFilenameChars(inStr).Should().Be(outStr);
}
}
[TestClass]
@ -160,22 +197,31 @@ namespace FileUtilityTests
[TestMethod]
// dot-files
[DataRow(@"C:\a bc\x y z\.f i l e.txt")]
[DataRow(@"C:\a bc\x y z\.f i l e.txt", PlatformID.Win32NT)]
[DataRow(@"/a bc/x y z/.f i l e.txt", PlatformID.Unix)]
// dot-folders
[DataRow(@"C:\a bc\.x y z\f i l e.txt")]
public void Valid(string input) => Tests(input, input);
[DataRow(@"C:\a bc\.x y z\f i l e.txt", PlatformID.Win32NT)]
[DataRow(@"/a bc/.x y z/f i l e.txt", PlatformID.Unix)]
public void Valid(string input, PlatformID platformID) => Tests(input, input, platformID);
[TestMethod]
// folder spaces
[DataRow(@"C:\ a bc \x y z ", @"C:\a bc\x y z")]
[DataRow(@"C:\ a bc \x y z ", @"C:\a bc\x y z", PlatformID.Win32NT)]
[DataRow(@"/ a bc /x y z ", @"/a bc/x y z", PlatformID.Unix)]
// file spaces
[DataRow(@"C:\a bc\x y z\ f i l e.txt ", @"C:\a bc\x y z\f i l e.txt")]
[DataRow(@"C:\a bc\x y z\ f i l e.txt ", @"C:\a bc\x y z\f i l e.txt", PlatformID.Win32NT)]
[DataRow(@"/a bc/x y z/ f i l e.txt ", @"/a bc/x y z/f i l e.txt", PlatformID.Unix)]
// eliminate beginning space and end dots and spaces
[DataRow(@"C:\a bc\ . . . x y z . . . \f i l e.txt", @"C:\a bc\. . . x y z\f i l e.txt")]
[DataRow(@"C:\a bc\ . . . x y z . . . \f i l e.txt", @"C:\a bc\. . . x y z\f i l e.txt", PlatformID.Win32NT)]
[DataRow(@"/a bc/ . . . x y z . . . /f i l e.txt", @"/a bc/. . . x y z/f i l e.txt", PlatformID.Unix)]
// file end dots
[DataRow(@"C:\a bc\x y z\f i l e.txt . . .", @"C:\a bc\x y z\f i l e.txt")]
public void Tests(string input, string expected)
=> FileUtility.GetValidFilename(input, Replacements).PathWithoutPrefix.Should().Be(expected);
[DataRow(@"C:\a bc\x y z\f i l e.txt . . .", @"C:\a bc\x y z\f i l e.txt", PlatformID.Win32NT)]
[DataRow(@"/a bc/x y z/f i l e.txt . . .", @"/a bc/x y z/f i l e.txt", PlatformID.Unix)]
public void Tests(string input, string expected, PlatformID platformID)
{
if (Environment.OSVersion.Platform == platformID)
FileUtility.GetValidFilename(input, Replacements).PathWithoutPrefix.Should().Be(expected);
}
}
[TestClass]

View File

@ -81,40 +81,62 @@ namespace TemplatesTests
=> Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension);
[TestMethod]
public void null_extension() => Tests("f.txt", @"C:\foo\bar", null, @"C:\foo\bar\f.txt");
[TestMethod]
[DataRow("f.txt", @"C:\foo\bar", "ext", @"C:\foo\bar\f.txt.ext")]
[DataRow("f", @"C:\foo\bar", "ext", @"C:\foo\bar\f.ext")]
[DataRow("<id>", @"C:\foo\bar", "ext", @"C:\foo\bar\asin.ext")]
[DataRow("<bitrate> - <samplerate> - <channels>", @"C:\foo\bar", "ext", @"C:\foo\bar\128 - 44100 - 2.ext")]
[DataRow("<year> - <channels>", @"C:\foo\bar", "ext", @"C:\foo\bar\2017 - 2.ext")]
public void Tests(string template, string dirFullPath, string extension, string expected)
=> Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension)
[DataRow("f.txt", @"C:\foo\bar", null, @"C:\foo\bar\f.txt", PlatformID.Win32NT)]
[DataRow("f.txt", @"/foo/bar", null, @"/foo/bar/f.txt", PlatformID.Unix)]
[DataRow("f.txt", @"C:\foo\bar", "ext", @"C:\foo\bar\f.txt.ext", PlatformID.Win32NT)]
[DataRow("f.txt", @"/foo/bar", "ext", @"/foo/bar/f.txt.ext", PlatformID.Unix)]
[DataRow("f", @"C:\foo\bar", "ext", @"C:\foo\bar\f.ext", PlatformID.Win32NT)]
[DataRow("f", @"/foo/bar", "ext", @"/foo/bar/f.ext", PlatformID.Unix)]
[DataRow("<id>", @"C:\foo\bar", "ext", @"C:\foo\bar\asin.ext", PlatformID.Win32NT)]
[DataRow("<id>", @"/foo/bar", "ext", @"/foo/bar/asin.ext", PlatformID.Unix)]
[DataRow("<bitrate> - <samplerate> - <channels>", @"C:\foo\bar", "ext", @"C:\foo\bar\128 - 44100 - 2.ext", PlatformID.Win32NT)]
[DataRow("<bitrate> - <samplerate> - <channels>", @"/foo/bar", "ext", @"/foo/bar/128 - 44100 - 2.ext", PlatformID.Unix)]
[DataRow("<year> - <channels>", @"C:\foo\bar", "ext", @"C:\foo\bar\2017 - 2.ext", PlatformID.Win32NT)]
[DataRow("<year> - <channels>", @"/foo/bar", "ext", @"/foo/bar/2017 - 2.ext", PlatformID.Unix)]
public void Tests(string template, string dirFullPath, string extension, string expected, PlatformID platformID)
{
if (Environment.OSVersion.Platform == platformID)
Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension)
.GetFilePath(Replacements)
.PathWithoutPrefix
.Should().Be(expected);
}
[TestMethod]
public void IfSeries_empty()
=> Templates.getFileNamingTemplate(GetLibraryBook(), "foo<if series-><-if series>bar", @"C:\a\b", "ext")
[DataRow(@"C:\a\b", @"C:\a\b\foobar.ext", PlatformID.Win32NT)]
[DataRow(@"/a/b", @"/a/b/foobar.ext", PlatformID.Unix)]
public void IfSeries_empty(string directory, string expected, PlatformID platformID)
{
if (Environment.OSVersion.Platform == platformID)
Templates.getFileNamingTemplate(GetLibraryBook(), "foo<if series-><-if series>bar", directory, "ext")
.GetFilePath(Replacements)
.PathWithoutPrefix
.Should().Be(@"C:\a\b\foobar.ext");
.Should().Be(expected);
}
[TestMethod]
public void IfSeries_no_series()
=> Templates.getFileNamingTemplate(GetLibraryBook(null), "foo<if series->-<series>-<id>-<-if series>bar", @"C:\a\b", "ext")
[DataRow(@"C:\a\b", @"C:\a\b\foobar.ext", PlatformID.Win32NT)]
[DataRow(@"/a/b", @"/a/b/foobar.ext", PlatformID.Unix)]
public void IfSeries_no_series(string directory, string expected, PlatformID platformID)
{
if (Environment.OSVersion.Platform == platformID)
Templates.getFileNamingTemplate(GetLibraryBook(null), "foo<if series->-<series>-<id>-<-if series>bar", directory, "ext")
.GetFilePath(Replacements)
.PathWithoutPrefix
.Should().Be(@"C:\a\b\foobar.ext");
.Should().Be(expected);
}
[TestMethod]
public void IfSeries_with_series()
=> Templates.getFileNamingTemplate(GetLibraryBook(), "foo<if series->-<series>-<id>-<-if series>bar", @"C:\a\b", "ext")
[DataRow(@"C:\a\b", @"C:\a\b\foo-Sherlock Holmes-asin-bar.ext", PlatformID.Win32NT)]
[DataRow(@"/a/b", @"/a/b/foo-Sherlock Holmes-asin-bar.ext", PlatformID.Unix)]
public void IfSeries_with_series(string directory, string expected, PlatformID platformID)
{
if (Environment.OSVersion.Platform == platformID)
Templates.getFileNamingTemplate(GetLibraryBook(), "foo<if series->-<series>-<id>-<-if series>bar", directory, "ext")
.GetFilePath(Replacements)
.PathWithoutPrefix
.Should().Be(@"C:\a\b\foo-Sherlock Holmes-asin-bar.ext");
.Should().Be(expected);
}
}
}
@ -256,7 +278,7 @@ namespace Templates_File_Tests
public class GetErrors
{
[TestMethod]
public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID });
public void null_is_invalid() => Tests(null, Environment.OSVersion.Platform, new[] { Templates.ERROR_NULL_IS_INVALID });
[TestMethod]
public void empty_is_valid() => valid_tests("");
@ -267,41 +289,53 @@ namespace Templates_File_Tests
[TestMethod]
[DataRow(@"foo")]
[DataRow(@"<id>")]
public void valid_tests(string template) => Tests(template, Array.Empty<string>());
public void valid_tests(string template) => Tests(template, Environment.OSVersion.Platform, Array.Empty<string>());
[TestMethod]
[DataRow(@"C:\", Templates.ERROR_INVALID_FILE_NAME_CHAR)]
[DataRow(@"\foo", Templates.ERROR_INVALID_FILE_NAME_CHAR)]
[DataRow(@"/foo", Templates.ERROR_INVALID_FILE_NAME_CHAR)]
[DataRow(@"C:\", Templates.ERROR_INVALID_FILE_NAME_CHAR)]
public void Tests(string template, params string[] expected)
[DataRow(@"C:\", PlatformID.Win32NT, Templates.ERROR_INVALID_FILE_NAME_CHAR)]
[DataRow(@"/", PlatformID.Unix, Templates.ERROR_INVALID_FILE_NAME_CHAR)]
[DataRow(@"\foo", PlatformID.Win32NT, Templates.ERROR_INVALID_FILE_NAME_CHAR)]
[DataRow(@"/foo", PlatformID.Win32NT, Templates.ERROR_INVALID_FILE_NAME_CHAR)]
[DataRow(@"/foo", PlatformID.Unix, Templates.ERROR_INVALID_FILE_NAME_CHAR)]
public void Tests(string template, PlatformID platformID, params string[] expected)
{
if (Environment.OSVersion.Platform == platformID)
{
var result = Templates.File.GetErrors(template);
result.Count().Should().Be(expected.Length);
result.Should().BeEquivalentTo(expected);
}
}
}
[TestClass]
public class IsValid
{
[TestMethod]
public void null_is_invalid() => Tests(null, false);
public void null_is_invalid() => Tests(null, false, Environment.OSVersion.Platform);
[TestMethod]
public void empty_is_valid() => Tests("", true);
public void empty_is_valid() => Tests("", true, Environment.OSVersion.Platform);
[TestMethod]
public void whitespace_is_valid() => Tests(" ", true);
public void whitespace_is_valid() => Tests(" ", true, Environment.OSVersion.Platform);
[TestMethod]
[DataRow(@"C:\", false)]
[DataRow(@"foo", true)]
[DataRow(@"\foo", false)]
[DataRow(@"/foo", false)]
[DataRow(@"<id>", true)]
public void Tests(string template, bool expected) => Templates.File.IsValid(template).Should().Be(expected);
[DataRow(@"C:\", false, PlatformID.Win32NT)]
[DataRow(@"/", false, PlatformID.Unix)]
[DataRow(@"foo", true, PlatformID.Win32NT)]
[DataRow(@"foo", true, PlatformID.Unix)]
[DataRow(@"\foo", false, PlatformID.Win32NT)]
[DataRow(@"\foo", true, PlatformID.Unix)]
[DataRow(@"/foo", false, PlatformID.Win32NT)]
[DataRow(@"<id>", true, PlatformID.Win32NT)]
[DataRow(@"<id>", true, PlatformID.Unix)]
public void Tests(string template, bool expected, PlatformID platformID)
{
if (Environment.OSVersion.Platform == platformID)
Templates.File.IsValid(template).Should().Be(expected);
}
}
// same as Templates.Folder.GetWarnings
@ -331,30 +365,36 @@ namespace Templates_ChapterFile_Tests
public class GetWarnings
{
[TestMethod]
public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID });
public void null_is_invalid() => Tests(null, null, new[] { Templates.ERROR_NULL_IS_INVALID });
[TestMethod]
public void empty_has_warnings() => Tests("", Templates.WARNING_EMPTY, Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG);
public void empty_has_warnings() => Tests("", null, Templates.WARNING_EMPTY, Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG);
[TestMethod]
public void whitespace_has_warnings() => Tests(" ", Templates.WARNING_WHITE_SPACE, Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG);
public void whitespace_has_warnings() => Tests(" ", null, Templates.WARNING_WHITE_SPACE, Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG);
[TestMethod]
[DataRow("<ch#>")]
[DataRow("<ch#> <id>")]
public void valid_tests(string template) => Tests(template, Array.Empty<string>());
public void valid_tests(string template) => Tests(template, null, Array.Empty<string>());
[TestMethod]
[DataRow(@"no tags", Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
[DataRow(@"<id>\foo\bar", Templates.ERROR_INVALID_FILE_NAME_CHAR, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
[DataRow("<chapter count> -- chapter tag but not ch# or ch_#", Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
public void Tests(string template, params string[] expected)
[DataRow(@"no tags", null, Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
[DataRow(@"<id>\foo\bar", true, Templates.ERROR_INVALID_FILE_NAME_CHAR, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
[DataRow(@"<id>/foo/bar", false, Templates.ERROR_INVALID_FILE_NAME_CHAR, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
[DataRow("<chapter count> -- chapter tag but not ch# or ch_#", null, Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)]
public void Tests(string template, bool? windows, params string[] expected)
{
if(windows is null
|| (windows is true && Environment.OSVersion.Platform is PlatformID.Win32NT)
|| (windows is false && Environment.OSVersion.Platform is PlatformID.Unix))
{
var result = Templates.ChapterFile.GetWarnings(template);
result.Count().Should().Be(expected.Length);
result.Should().BeEquivalentTo(expected);
}
}
}
[TestClass]
public class HasWarnings
@ -408,10 +448,15 @@ namespace Templates_ChapterFile_Tests
static readonly ReplacementCharacters Default = ReplacementCharacters.Default;
[TestMethod]
[DataRow("[<id>] <ch# 0> of <ch count> - <ch title>", @"C:\foo\", "txt", 6, 10, "chap", @"C:\foo\[asin] 06 of 10 - chap.txt")]
[DataRow("<ch#>", @"C:\foo\", "txt", 6, 10, "chap", @"C:\foo\6.txt")]
public void Tests(string template, string dir, string ext, int pos, int total, string chapter, string expected)
=> Templates.ChapterFile.GetPortionFilename(GetLibraryBook(), template, new() { OutputFileName = $"xyz.{ext}", PartsPosition = pos, PartsTotal = total, Title = chapter }, dir, Default)
[DataRow("[<id>] <ch# 0> of <ch count> - <ch title>", @"C:\foo\", "txt", 6, 10, "chap", @"C:\foo\[asin] 06 of 10 - chap.txt", PlatformID.Win32NT)]
[DataRow("[<id>] <ch# 0> of <ch count> - <ch title>", @"/foo/", "txt", 6, 10, "chap", @"/foo/[asin] 06 of 10 - chap.txt", PlatformID.Unix)]
[DataRow("<ch#>", @"C:\foo\", "txt", 6, 10, "chap", @"C:\foo\6.txt", PlatformID.Win32NT)]
[DataRow("<ch#>", @"/foo/", "txt", 6, 10, "chap", @"/foo/6.txt", PlatformID.Unix)]
public void Tests(string template, string dir, string ext, int pos, int total, string chapter, string expected, PlatformID platformID)
{
if (Environment.OSVersion.Platform == platformID)
Templates.ChapterFile.GetPortionFilename(GetLibraryBook(), template, new() { OutputFileName = $"xyz.{ext}", PartsPosition = pos, PartsTotal = total, Title = chapter }, dir, Default)
.Should().Be(expected);
}
}
}