Bug fix: slashes in template values (eg: title) breaks file management #145

This commit is contained in:
Robert McRackan 2021-11-01 11:40:55 -04:00
parent cae8ca7ef3
commit 46b120ee41
6 changed files with 126 additions and 42 deletions

View File

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<Version>6.3.3.1</Version> <Version>6.3.4.1</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -45,8 +45,20 @@ namespace FileManager
.Replace(">", ""); .Replace(">", "");
private string formatValue(object value) private string formatValue(object value)
=> ParameterMaxSize.HasValue && ParameterMaxSize.Value > 0 {
? value?.ToString().Truncate(ParameterMaxSize.Value) if (value is null)
: value?.ToString(); return "";
// 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.
var val = value
.ToString()
.Replace($"{System.IO.Path.DirectorySeparatorChar}", IllegalCharacterReplacements)
.Replace($"{System.IO.Path.AltDirectorySeparatorChar}", IllegalCharacterReplacements);
return
ParameterMaxSize.HasValue && ParameterMaxSize.Value > 0
? val.Truncate(ParameterMaxSize.Value)
: val;
}
} }
} }

View File

@ -16,8 +16,6 @@ namespace LibationFileManager
public const string WARNING_NO_TAGS = "Should use tags. Eg: <title>"; public const string WARNING_NO_TAGS = "Should use tags. Eg: <title>";
public const string WARNING_HAS_CHAPTER_TAGS = "Chapter tags should only be used in the template used for naming files which are split by chapter. Eg: <ch title>"; public const string WARNING_HAS_CHAPTER_TAGS = "Chapter tags should only be used in the template used for naming files which are split by chapter. Eg: <ch title>";
public const string WARNING_NO_CHAPTER_NUMBER_TAG = "Should include chapter number tag in template used for naming files which are split by chapter. Ie: <ch#> or <ch# 0>"; public const string WARNING_NO_CHAPTER_NUMBER_TAG = "Should include chapter number tag in template used for naming files which are split by chapter. Ie: <ch#> or <ch# 0>";
// actual possible to se?
public const string WARNING_NO_CHAPTER_TAGS = "Should include chapter tags in template used for naming files which are split by chapter. Eg: <ch title>";
public static Templates Folder { get; } = new FolderTemplate(); public static Templates Folder { get; } = new FolderTemplate();
public static Templates File { get; } = new FileTemplate(); public static Templates File { get; } = new FileTemplate();
@ -26,6 +24,7 @@ namespace LibationFileManager
public abstract string Name { get; } public abstract string Name { get; }
public abstract string Description { get; } public abstract string Description { get; }
public abstract string DefaultTemplate { get; } public abstract string DefaultTemplate { get; }
protected abstract bool IsChapterized { get; }
public abstract IEnumerable<string> GetErrors(string template); public abstract IEnumerable<string> GetErrors(string template);
public bool IsValid(string template) => !GetErrors(template).Any(); public bool IsValid(string template) => !GetErrors(template).Any();
@ -33,7 +32,17 @@ namespace LibationFileManager
public abstract IEnumerable<string> GetWarnings(string template); public abstract IEnumerable<string> GetWarnings(string template);
public bool HasWarnings(string template) => GetWarnings(template).Any(); public bool HasWarnings(string template) => GetWarnings(template).Any();
public abstract int TagCount(string template); public IEnumerable<TemplateTags> GetTemplateTags()
=> TemplateTags.GetAll()
// yeah, this line is a little funky but it works when you think through it. also: trust the unit tests
.Where(t => IsChapterized || !t.IsChapterOnly);
public int TagCount(string template)
=> GetTemplateTags()
// for <id><id> == 1, use:
// .Count(t => template.Contains($"<{t.TagName}>"))
// .Sum() impl: <id><id> == 2
.Sum(t => template.Split($"<{t.TagName}>").Length - 1);
public static bool ContainsChapterOnlyTags(string template) public static bool ContainsChapterOnlyTags(string template)
=> TemplateTags.GetAll() => TemplateTags.GetAll()
@ -59,7 +68,7 @@ namespace LibationFileManager
return Valid; return Valid;
} }
protected IEnumerable<string> getWarnings(string template, bool isChapter) protected IEnumerable<string> getWarnings(string template)
{ {
var warnings = GetErrors(template).ToList(); var warnings = GetErrors(template).ToList();
if (template is null) if (template is null)
@ -73,28 +82,18 @@ namespace LibationFileManager
if (TagCount(template) == 0) if (TagCount(template) == 0)
warnings.Add(WARNING_NO_TAGS); warnings.Add(WARNING_NO_TAGS);
var containsChapterOnlyTags = ContainsChapterOnlyTags(template); if (!IsChapterized && ContainsChapterOnlyTags(template))
if (isChapter && !containsChapterOnlyTags)
warnings.Add(WARNING_NO_CHAPTER_TAGS);
if (!isChapter && containsChapterOnlyTags)
warnings.Add(WARNING_HAS_CHAPTER_TAGS); warnings.Add(WARNING_HAS_CHAPTER_TAGS);
return warnings; return warnings;
} }
protected static int tagCount(string template, Func<TemplateTags, bool> func)
=> TemplateTags.GetAll()
.Where(func)
// for <id><id> == 1, use:
// .Count(t => template.Contains($"<{t.TagName}>"))
// .Sum() impl: <id><id> == 2
.Sum(t => template.Split($"<{t.TagName}>").Length - 1);
private class FolderTemplate : Templates private class FolderTemplate : Templates
{ {
public override string Name => "Folder Template"; public override string Name => "Folder Template";
public override string Description => Configuration.GetDescription(nameof(Configuration.FolderTemplate)); public override string Description => Configuration.GetDescription(nameof(Configuration.FolderTemplate));
public override string DefaultTemplate { get; } = "<title short> [<id>]"; public override string DefaultTemplate { get; } = "<title short> [<id>]";
protected override bool IsChapterized { get; } = false;
public override IEnumerable<string> GetErrors(string template) public override IEnumerable<string> GetErrors(string template)
{ {
@ -109,9 +108,7 @@ namespace LibationFileManager
return Valid; return Valid;
} }
public override IEnumerable<string> GetWarnings(string template) => getWarnings(template, false); public override IEnumerable<string> GetWarnings(string template) => getWarnings(template);
public override int TagCount(string template) => tagCount(template, t => !t.IsChapterOnly);
} }
private class FileTemplate : Templates private class FileTemplate : Templates
@ -119,12 +116,11 @@ namespace LibationFileManager
public override string Name => "File Template"; public override string Name => "File Template";
public override string Description => Configuration.GetDescription(nameof(Configuration.FileTemplate)); public override string Description => Configuration.GetDescription(nameof(Configuration.FileTemplate));
public override string DefaultTemplate { get; } = "<title> [<id>]"; public override string DefaultTemplate { get; } = "<title> [<id>]";
protected override bool IsChapterized { get; } = false;
public override IEnumerable<string> GetErrors(string template) => getFileErrors(template); public override IEnumerable<string> GetErrors(string template) => getFileErrors(template);
public override IEnumerable<string> GetWarnings(string template) => getWarnings(template, false); public override IEnumerable<string> GetWarnings(string template) => getWarnings(template);
public override int TagCount(string template) => tagCount(template, t => !t.IsChapterOnly);
} }
private class ChapterFileTemplate : Templates private class ChapterFileTemplate : Templates
@ -132,26 +128,22 @@ namespace LibationFileManager
public override string Name => "Chapter File Template"; public override string Name => "Chapter File Template";
public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)); public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate));
public override string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>"; public override string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>";
protected override bool IsChapterized { get; } = true;
public override IEnumerable<string> GetErrors(string template) => getFileErrors(template); public override IEnumerable<string> GetErrors(string template) => getFileErrors(template);
public override IEnumerable<string> GetWarnings(string template) public override IEnumerable<string> GetWarnings(string template)
{ {
var warnings = getWarnings(template, true).ToList(); var warnings = getWarnings(template).ToList();
if (template is null) if (template is null)
return warnings; return warnings;
// recommended to incl. <ch#> or <ch# 0> // recommended to incl. <ch#> or <ch# 0>
if (!ContainsTag(template, TemplateTags.ChNumber.TagName) && !ContainsTag(template, TemplateTags.ChNumber0.TagName)) if (!ContainsTag(template, TemplateTags.ChNumber.TagName) && !ContainsTag(template, TemplateTags.ChNumber0.TagName))
{
warnings.Remove(WARNING_NO_CHAPTER_TAGS);
warnings.Add(WARNING_NO_CHAPTER_NUMBER_TAG); warnings.Add(WARNING_NO_CHAPTER_NUMBER_TAG);
}
return warnings; return warnings;
} }
public override int TagCount(string template) => tagCount(template, t => true);
} }
} }
} }

View File

@ -34,6 +34,9 @@
this.templateLbl = new System.Windows.Forms.Label(); this.templateLbl = new System.Windows.Forms.Label();
this.resetToDefaultBtn = new System.Windows.Forms.Button(); this.resetToDefaultBtn = new System.Windows.Forms.Button();
this.outputTb = new System.Windows.Forms.TextBox(); this.outputTb = new System.Windows.Forms.TextBox();
this.listView1 = new System.Windows.Forms.ListView();
this.columnHeader1 = new System.Windows.Forms.ColumnHeader();
this.columnHeader2 = new System.Windows.Forms.ColumnHeader();
this.SuspendLayout(); this.SuspendLayout();
// //
// saveBtn // saveBtn
@ -93,12 +96,41 @@
// //
// outputTb // outputTb
// //
this.outputTb.Location = new System.Drawing.Point(12, 153); this.outputTb.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.outputTb.Location = new System.Drawing.Point(346, 56);
this.outputTb.Multiline = true; this.outputTb.Multiline = true;
this.outputTb.Name = "outputTb"; this.outputTb.Name = "outputTb";
this.outputTb.ReadOnly = true; this.outputTb.ReadOnly = true;
this.outputTb.Size = new System.Drawing.Size(759, 205); this.outputTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.outputTb.TabIndex = 100; this.outputTb.Size = new System.Drawing.Size(574, 434);
this.outputTb.TabIndex = 4;
//
// listView1
//
this.listView1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)));
this.listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.columnHeader1,
this.columnHeader2});
this.listView1.HideSelection = false;
this.listView1.Location = new System.Drawing.Point(12, 56);
this.listView1.Name = "listView1";
this.listView1.Size = new System.Drawing.Size(328, 434);
this.listView1.TabIndex = 100;
this.listView1.UseCompatibleStateImageBehavior = false;
this.listView1.View = System.Windows.Forms.View.Details;
//
// columnHeader1
//
this.columnHeader1.Text = "Tag";
this.columnHeader1.Width = 90;
//
// columnHeader2
//
this.columnHeader2.Text = "Description";
this.columnHeader2.Width = 230;
// //
// EditTemplateDialog // EditTemplateDialog
// //
@ -107,6 +139,7 @@
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancelBtn; this.CancelButton = this.cancelBtn;
this.ClientSize = new System.Drawing.Size(933, 539); this.ClientSize = new System.Drawing.Size(933, 539);
this.Controls.Add(this.listView1);
this.Controls.Add(this.outputTb); this.Controls.Add(this.outputTb);
this.Controls.Add(this.resetToDefaultBtn); this.Controls.Add(this.resetToDefaultBtn);
this.Controls.Add(this.templateLbl); this.Controls.Add(this.templateLbl);
@ -131,5 +164,8 @@
private System.Windows.Forms.Label templateLbl; private System.Windows.Forms.Label templateLbl;
private System.Windows.Forms.Button resetToDefaultBtn; private System.Windows.Forms.Button resetToDefaultBtn;
private System.Windows.Forms.TextBox outputTb; private System.Windows.Forms.TextBox outputTb;
private System.Windows.Forms.ListView listView1;
private System.Windows.Forms.ColumnHeader columnHeader1;
private System.Windows.Forms.ColumnHeader columnHeader2;
} }
} }

View File

@ -39,16 +39,32 @@ namespace LibationWinForms.Dialogs
this.templateLbl.Text = template.Description; this.templateLbl.Text = template.Description;
this.templateTb.Text = inputTemplateText; this.templateTb.Text = inputTemplateText;
// populate list view
foreach (var tag in template.GetTemplateTags())
listView1.Items.Add(new ListViewItem(new[] { $"<{tag.TagName}>", tag.Description }));
} }
private void resetToDefaultBtn_Click(object sender, EventArgs e) => templateTb.Text = template.DefaultTemplate; private void resetToDefaultBtn_Click(object sender, EventArgs e) => templateTb.Text = template.DefaultTemplate;
private void templateTb_TextChanged(object sender, EventArgs e) private void templateTb_TextChanged(object sender, EventArgs e)
{ {
var t = templateTb.Text;
var warnings
= !template.HasWarnings(t)
? ""
: "Warnings:\r\n" +
template
.GetWarnings(t)
.Select(err => $"- {err}")
.Aggregate((a, b) => $"{a}\r\n{b}");
var books = config.Books; var books = config.Books;
var folderTemplate = template == Templates.Folder ? templateTb.Text : config.FolderTemplate; var folderTemplate = template == Templates.Folder ? t : config.FolderTemplate;
folderTemplate = folderTemplate.Trim().Trim(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }).Trim(); folderTemplate = folderTemplate.Trim().Trim(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }).Trim();
var fileTemplate = template == Templates.Folder ? config.FileTemplate : templateTb.Text; var fileTemplate = template == Templates.Folder ? config.FileTemplate : t;
fileTemplate = fileTemplate.Trim(); fileTemplate = fileTemplate.Trim();
var ext = config.DecryptToLossy ? "mp3" : "m4b"; var ext = config.DecryptToLossy ? "mp3" : "m4b";
@ -56,13 +72,23 @@ namespace LibationWinForms.Dialogs
// this logic should be external // this logic should be external
path = path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); path = path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
var sing = $"{Path.DirectorySeparatorChar}";
var dbl = $"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}"; var dbl = $"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}";
while (path.Contains(dbl)) while (path.Contains(dbl))
path = path.Replace(dbl, $"{Path.DirectorySeparatorChar}"); path = path.Replace(dbl, sing);
// once path is finalized
const char ZERO_WIDTH_SPACE = '\u200B';
path = path.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
// result: can wrap long paths. eg:
// |-- LINE WRAP BOUNDARIES --|
// \books\author with a very <= normal line break on space between words
// long name\narrator narrator
// \title <= line break on the zero-with space we added before slashes
var book = new DataLayer.Book( var book = new DataLayer.Book(
new DataLayer.AudibleProductId("123456789"), new DataLayer.AudibleProductId("123456789"),
"A Study in Scarlet", "A Study in Scarlet: A Sherlock Holmes Novel",
"Fake description", "Fake description",
1234, 1234,
DataLayer.ContentType.Product, DataLayer.ContentType.Product,
@ -78,6 +104,9 @@ namespace LibationWinForms.Dialogs
var libraryBook = new DataLayer.LibraryBook(book, DateTime.Now, "my account"); var libraryBook = new DataLayer.LibraryBook(book, DateTime.Now, "my account");
outputTb.Text = @$" outputTb.Text = @$"
Example:
{books} {books}
{folderTemplate} {folderTemplate}
{fileTemplate} {fileTemplate}
@ -89,14 +118,21 @@ namespace LibationWinForms.Dialogs
{book.AuthorNames} {book.AuthorNames}
{book.NarratorNames} {book.NarratorNames}
series: {"Sherlock Holmes"} series: {"Sherlock Holmes"}
";
{warnings}
".Trim();
} }
private void saveBtn_Click(object sender, EventArgs e) private void saveBtn_Click(object sender, EventArgs e)
{ {
if (!template.IsValid(templateTb.Text)) if (!template.IsValid(templateTb.Text))
{ {
MessageBox.Show("This template text is not valid.", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error); var errors = template
.GetErrors(templateTb.Text)
.Select(err => $"- {err}")
.Aggregate((a, b) => $"{a}\r\n{b}");
MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
return; return;
} }

View File

@ -104,5 +104,13 @@ namespace FileTemplateTests
return fileTemplate.GetFilePath(); return fileTemplate.GetFilePath();
} }
[TestMethod]
public void remove_slashes()
{
var fileTemplate = new FileTemplate(@"\foo\<title>.txt");
fileTemplate.AddParameterReplacement("title", @"s\l/a\s/h\e/s");
fileTemplate.GetFilePath().Should().Be(@"\foo\slashes.txt");
}
} }
} }