diff --git a/AaxDecrypter/AaxDecrypter.csproj b/AaxDecrypter/AaxDecrypter.csproj
index 7629f675..f1d5f136 100644
--- a/AaxDecrypter/AaxDecrypter.csproj
+++ b/AaxDecrypter/AaxDecrypter.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/AaxDecrypter/AudiobookDownloadBase.cs b/AaxDecrypter/AudiobookDownloadBase.cs
index 3c2e638b..3a2df566 100644
--- a/AaxDecrypter/AudiobookDownloadBase.cs
+++ b/AaxDecrypter/AudiobookDownloadBase.cs
@@ -32,7 +32,7 @@ namespace AaxDecrypter
private NetworkFileStreamPersister nfsPersister;
private string jsonDownloadState => Path.Combine(CacheDir, Path.GetFileNameWithoutExtension(OutputFileName) + ".json");
- private string tempFile => PathLib.ReplaceExtension(jsonDownloadState, ".tmp");
+ private string tempFile => Path.ChangeExtension(jsonDownloadState, ".tmp");
public AudiobookDownloadBase(string outFileName, string cacheDirectory, DownloadLicense dlLic)
{
@@ -102,7 +102,7 @@ namespace AaxDecrypter
// not a critical step. its failure should not prevent future steps from running
try
{
- var path = PathLib.ReplaceExtension(OutputFileName, ".cue");
+ var path = Path.ChangeExtension(OutputFileName, ".cue");
path = FileUtility.GetValidFilename(path);
File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadLicense.ChapterInfo));
OnFileCreated(path);
diff --git a/AaxDecrypter/UnencryptedAudiobookDownloader.cs b/AaxDecrypter/UnencryptedAudiobookDownloader.cs
index 19fee327..55c7626c 100644
--- a/AaxDecrypter/UnencryptedAudiobookDownloader.cs
+++ b/AaxDecrypter/UnencryptedAudiobookDownloader.cs
@@ -19,8 +19,8 @@ namespace AaxDecrypter
["Step 1: Get Mp3 Metadata"] = Step1_GetMetadata,
["Step 2: Download Audiobook"] = Step2_DownloadAudiobookAsSingleFile,
- ["Step 3: Create Cue"] = Step3_CreateCue,
- ["Step 4: Cleanup"] = Step4_Cleanup,
+ ["Step 3: Create Cue"] = Step_CreateCue,
+ ["Step 4: Cleanup"] = Step_Cleanup,
};
}
@@ -66,9 +66,9 @@ namespace AaxDecrypter
CloseInputFileStream();
- var realOutputFileName = FileUtility.Move(InputFileStream.SaveFilePath, OutputFileName);
+ var realOutputFileName = FileUtility.SaferMoveToValidPath(InputFileStream.SaveFilePath, OutputFileName);
SetOutputFileName(realOutputFileName);
- OnFileCreated(OutputFileName);
+ OnFileCreated(realOutputFileName);
return !IsCanceled;
}
diff --git a/AppScaffolding/AppScaffolding.csproj b/AppScaffolding/AppScaffolding.csproj
index 981b7c52..f7fbc3f1 100644
--- a/AppScaffolding/AppScaffolding.csproj
+++ b/AppScaffolding/AppScaffolding.csproj
@@ -3,7 +3,7 @@
net5.0
- 6.2.6.8
+ 6.2.6.41
diff --git a/DataLayer/DataLayer.csproj b/DataLayer/DataLayer.csproj
index 3250720f..41399a80 100644
--- a/DataLayer/DataLayer.csproj
+++ b/DataLayer/DataLayer.csproj
@@ -12,7 +12,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/FileLiberator/ConvertToMp3.cs b/FileLiberator/ConvertToMp3.cs
index 42dc1570..106d4072 100644
--- a/FileLiberator/ConvertToMp3.cs
+++ b/FileLiberator/ConvertToMp3.cs
@@ -16,7 +16,7 @@ namespace FileLiberator
private Mp4File m4bBook;
private long fileSize;
- private static string Mp3FileName(string m4bPath) => m4bPath is null ? string.Empty : PathLib.ReplaceExtension(m4bPath, ".mp3");
+ private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3");
public override void Cancel() => m4bBook?.Cancel();
@@ -52,7 +52,7 @@ namespace FileLiberator
mp3File.Close();
var proposedMp3Path = Mp3FileName(m4bPath);
- var realMp3Path = FileUtility.Move(mp3File.Name, proposedMp3Path);
+ var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path);
OnFileCreated(libraryBook, realMp3Path);
var statusHandler = new StatusHandler();
diff --git a/FileLiberator/DownloadDecryptBook.cs b/FileLiberator/DownloadDecryptBook.cs
index 3262919a..74f904ba 100644
--- a/FileLiberator/DownloadDecryptBook.cs
+++ b/FileLiberator/DownloadDecryptBook.cs
@@ -198,7 +198,7 @@ namespace FileLiberator
{
var entry = entries[i];
- var realDest = FileUtility.Move(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)));
+ var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)));
FilePathCache.Insert(book.AudibleProductId, realDest);
// propogate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop)
diff --git a/FileLiberator/DownloadPdf.cs b/FileLiberator/DownloadPdf.cs
index fe27d5ba..9640a6b3 100644
--- a/FileLiberator/DownloadPdf.cs
+++ b/FileLiberator/DownloadPdf.cs
@@ -47,12 +47,7 @@ namespace FileLiberator
if (existingPath != null)
return Path.Combine(existingPath, Path.GetFileName(file));
- var full = FileUtility.GetValidFilename(
- AudibleFileStorage.PdfDirectory,
- libraryBook.Book.Title,
- Path.GetExtension(file),
- libraryBook.Book.AudibleProductId);
- return full;
+ return FileUtility.GetValidFilename(AudibleFileStorage.BooksDirectory, libraryBook.Book.Title, Path.GetExtension(file), libraryBook.Book.AudibleProductId);
}
private static string getdownloadUrl(LibraryBook libraryBook)
diff --git a/FileManager.Tests/FileManager.Tests.csproj b/FileManager.Tests/FileManager.Tests.csproj
index 2ae7e2bc..ec4c77ee 100644
--- a/FileManager.Tests/FileManager.Tests.csproj
+++ b/FileManager.Tests/FileManager.Tests.csproj
@@ -7,6 +7,7 @@
+
diff --git a/FileManager.Tests/FileTemplateTests.cs b/FileManager.Tests/FileTemplateTests.cs
new file mode 100644
index 00000000..061b50c0
--- /dev/null
+++ b/FileManager.Tests/FileTemplateTests.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Dinah.Core;
+using FileManager;
+using FluentAssertions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace FileTemplateTests
+{
+ [TestClass]
+ public class GetFilename
+ {
+ [TestMethod]
+ public void equiv_GetValidFilename()
+ {
+ var expected = @"C:\foo\bar\my_ book LONG_1234567890_1234567890_1234567890_123 [ID123456].txt";
+ var f1 = OLD_GetValidFilename(@"C:\foo\bar", "my: book LONG_1234567890_1234567890_1234567890_12345", "txt", "ID123456");
+ var f2 = NEW_GetValidFilename_FileTemplate(@"C:\foo\bar", "my: book LONG_1234567890_1234567890_1234567890_12345", "txt", "ID123456");
+
+ f1.Should().Be(expected);
+ f1.Should().Be(f2);
+ }
+ private static string OLD_GetValidFilename(string dirFullPath, string filename, string extension, string metadataSuffix)
+ {
+ ArgumentValidator.EnsureNotNullOrWhiteSpace(dirFullPath, nameof(dirFullPath));
+
+ filename ??= "";
+
+ // sanitize. omit invalid characters. exception: colon => underscore
+ filename = filename.Replace(":", "_");
+ filename = FileUtility.GetSafeFileName(filename);
+
+ if (filename.Length > 50)
+ filename = filename.Substring(0, 50);
+
+ if (!string.IsNullOrWhiteSpace(metadataSuffix))
+ filename += $" [{metadataSuffix}]";
+
+ // extension is null when this method is used for directory names
+ extension = FileUtility.GetStandardizedExtension(extension);
+
+ // ensure uniqueness
+ var fullfilename = Path.Combine(dirFullPath, filename + extension);
+ var i = 0;
+ while (File.Exists(fullfilename))
+ fullfilename = Path.Combine(dirFullPath, filename + $" ({++i})" + extension);
+
+ return fullfilename;
+ }
+ private static string NEW_GetValidFilename_FileTemplate(string dirFullPath, string filename, string extension, string metadataSuffix)
+ {
+ var template = $" []";
+
+ var fullfilename = Path.Combine(dirFullPath, template + FileUtility.GetStandardizedExtension(extension));
+
+ var fileTemplate = new FileTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
+ fileTemplate.AddParameterReplacement("title", filename);
+ fileTemplate.AddParameterReplacement("id", metadataSuffix);
+ return fileTemplate.GetFilename();
+ }
+
+ [TestMethod]
+ public void equiv_GetMultipartFileName()
+ {
+ var expected = @"C:\foo\bar\my file - 002 - title.txt";
+ var f1 = OLD_GetMultipartFileName(@"C:\foo\bar\my file.txt", 2, 100, "title");
+ var f2 = NEW_GetMultipartFileName_FileTemplate(@"C:\foo\bar\my file.txt", 2, 100, "title");
+
+ f1.Should().Be(expected);
+ f1.Should().Be(f2);
+ }
+ private static string OLD_GetMultipartFileName(string originalPath, int partsPosition, int partsTotal, string suffix)
+ {
+ // 1-9 => 1-9
+ // 10-99 => 01-99
+ // 100-999 => 001-999
+ var chapterCountLeadingZeros = partsPosition.ToString().PadLeft(partsTotal.ToString().Length, '0');
+
+ string extension = Path.GetExtension(originalPath);
+
+ var filenameBase = $"{Path.GetFileNameWithoutExtension(originalPath)} - {chapterCountLeadingZeros}";
+ if (!string.IsNullOrWhiteSpace(suffix))
+ filenameBase += $" - {suffix}";
+
+ // Replace illegal path characters with spaces
+ var fileName = FileUtility.GetSafeFileName(filenameBase, " ");
+ var path = Path.Combine(Path.GetDirectoryName(originalPath), fileName + extension);
+ return path;
+ }
+ private static string NEW_GetMultipartFileName_FileTemplate(string originalPath, int partsPosition, int partsTotal, string suffix)
+ {
+ // 1-9 => 1-9
+ // 10-99 => 01-99
+ // 100-999 => 001-999
+ var chapterCountLeadingZeros = partsPosition.ToString().PadLeft(partsTotal.ToString().Length, '0');
+
+ var t = Path.ChangeExtension(originalPath, null) + " - - " + Path.GetExtension(originalPath);
+
+ var fileTemplate = new FileTemplate(t) { IllegalCharacterReplacements = " " };
+ fileTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
+ fileTemplate.AddParameterReplacement("title", suffix);
+
+ return fileTemplate.GetFilename();
+ }
+ }
+}
diff --git a/FileManager.Tests/FileUtilityTests.cs b/FileManager.Tests/FileUtilityTests.cs
index cce74f3d..b8472a58 100644
--- a/FileManager.Tests/FileUtilityTests.cs
+++ b/FileManager.Tests/FileUtilityTests.cs
@@ -1,13 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Dinah.Core;
+using FileManager;
+using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FileUtilityTests
{
+ [TestClass]
+ public class GetSafePath
+ {
+ [TestMethod]
+ public void null_path_throws() => Assert.ThrowsException(() => FileUtility.GetSafePath(null));
+
+ // needs separate method. middle null param not running correctly in TestExplorer when used in DataRow()
+ [TestMethod]
+ [DataRow("http://test.com/a/b/c", @"http\\test.com\a\b\c")]
+ public void null_replacement(string inStr, string outStr) => Tests(inStr, null, outStr);
+
+ [TestMethod]
+ // empty replacement
+ [DataRow("abc*abc.txt", "", "abcabc.txt")]
+ // non-empty replacement
+ [DataRow("abc*abc.txt", "ZZZ", "abcZZZabc.txt")]
+ // standardize slashes
+ [DataRow(@"a/b\c/d", "Z", @"a\b\c\d")]
+ // remove illegal chars
+ [DataRow("a*?:z.txt", "Z", "aZZZz.txt")]
+ // retain drive letter path colon
+ [DataRow(@"C:\az.txt", "Z", @"C:\az.txt")]
+ // replace all other colongs
+ [DataRow(@"a\b:c\d.txt", "ZZZ", @"a\bZZZc\d.txt")]
+ public void Tests(string inStr, string replacement, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, replacement));
+ }
+
+ [TestClass]
+ public class GetSafeFileName
+ {
+ // 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) => ReplacementTests(inStr, null, outStr);
+
+ [TestMethod]
+ // empty replacement
+ [DataRow("http://test.com/a/b/c", "", "httptest.comabc")]
+ // single char replace
+ [DataRow("http://test.com/a/b/c", "_", "http___test.com_a_b_c")]
+ // multi char replace
+ [DataRow("http://test.com/a/b/c", "!!!", "http!!!!!!!!!test.com!!!a!!!b!!!c")]
+ public void ReplacementTests(string inStr, string replacement, string outStr) => FileUtility.GetSafeFileName(inStr, replacement).Should().Be(outStr);
+ }
+
[TestClass]
public class GetValidFilename
{
[TestMethod]
- public void TestMethod1()
- {
- }
+ [DataRow(null, "name", "ext", "suffix")]
+ [DataRow(@"C:\", null, "ext", "suffix")]
+ [ExpectedException(typeof(ArgumentNullException))]
+ public void arg_null_exception(string dirFullPath, string filename, string extension, string metadataSuffix)
+ => FileUtility.GetValidFilename(dirFullPath, filename, extension, metadataSuffix);
+
+ [TestMethod]
+ [DataRow("", "name", "ext", "suffix")]
+ [DataRow(" ", "name", "ext", "suffix")]
+ [DataRow(@"C:\", "", "ext", "suffix")]
+ [DataRow(@"C:\", " ", "ext", "suffix")]
+ [ExpectedException(typeof(ArgumentException))]
+ public void arg_exception(string dirFullPath, string filename, string extension, string metadataSuffix)
+ => FileUtility.GetValidFilename(dirFullPath, filename, extension, metadataSuffix);
+
+ [TestMethod]
+ public void null_extension() => Tests(@"C:\foo\bar", "my file", null, "meta", @"C:\foo\bar\my file [meta]");
+ [TestMethod]
+ public void null_metadataSuffix() => Tests(@"C:\foo\bar", "my file", "txt", null, @"C:\foo\bar\my file [].txt");
+
+ [TestMethod]
+ [DataRow(@"C:\foo\bar", "my file", "txt", "my id", @"C:\foo\bar\my file [my id].txt")]
+ [DataRow(@"C:\foo\bar", "my file", "txt", "", @"C:\foo\bar\my file [].txt")]
+ public void Tests(string dirFullPath, string filename, string extension, string metadataSuffix, string expected)
+ => FileUtility.GetValidFilename(dirFullPath, filename, extension, metadataSuffix).Should().Be(expected);
+ }
+
+ [TestClass]
+ public class GetMultipartFileName
+ {
+ [TestMethod]
+ public void null_path() => Assert.ThrowsException(() => FileUtility.GetMultipartFileName(null, 1, 1, ""));
+
+ [TestMethod]
+ public void null_suffix() => Tests(@"C:\foo\bar\my file.txt", 2, 100, null, @"C:\foo\bar\my file - 002 - .txt");
+
+ [TestMethod]
+ public void negative_partsPosition() => Assert.ThrowsException(()
+ => FileUtility.GetMultipartFileName("foo", -1, 2, "")
+ );
+ [TestMethod]
+ public void zero_partsPosition() => Assert.ThrowsException(()
+ => FileUtility.GetMultipartFileName("foo", 0, 2, "")
+ );
+
+ [TestMethod]
+ public void negative_partsTotal() => Assert.ThrowsException(()
+ => FileUtility.GetMultipartFileName("foo", 2, -1, "")
+ );
+ [TestMethod]
+ public void zero_partsTotal() => Assert.ThrowsException(()
+ => FileUtility.GetMultipartFileName("foo", 2, 0, "")
+ );
+
+ [TestMethod]
+ public void partsPosition_greater_than_partsTotal() => Assert.ThrowsException(()
+ => FileUtility.GetMultipartFileName("foo", 2, 1, "")
+ );
+
+ [TestMethod]
+ // only part
+ [DataRow(@"C:\foo\bar\my file.txt", 1, 1, "title", @"C:\foo\bar\my file - 1 - title.txt")]
+ // 3 digits
+ [DataRow(@"C:\foo\bar\my file.txt", 2, 100, "title", @"C:\foo\bar\my file - 002 - title.txt")]
+ // no suffix
+ [DataRow(@"C:\foo\bar\my file.txt", 2, 100, "", @"C:\foo\bar\my file - 002 - .txt")]
+ public void Tests(string originalPath, int partsPosition, int partsTotal, string suffix, string expected)
+ => FileUtility.GetMultipartFileName(originalPath, partsPosition, partsTotal, suffix).Should().Be(expected);
}
}
diff --git a/FileManager/FileManager.csproj b/FileManager/FileManager.csproj
index eda85125..a98e4e01 100644
--- a/FileManager/FileManager.csproj
+++ b/FileManager/FileManager.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/FileManager/FileTemplate.cs b/FileManager/FileTemplate.cs
new file mode 100644
index 00000000..5dff519e
--- /dev/null
+++ b/FileManager/FileTemplate.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Dinah.Core;
+
+namespace FileManager
+{
+ /// Get valid filename. Advanced features incl. parameterized template
+ public class FileTemplate
+ {
+ /// Proposed full file path. May contain optional html-styled template tags. Eg: <name>
+ public string Template { get; }
+
+ /// Proposed file name with optional html-styled template tags.
+ public FileTemplate(string template) => Template = ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
+
+ /// Optional step 1: Replace html-styled template tags with parameters. Eg {"name", "Bill Gates"} => /<name>/ => /Bill Gates/
+ public Dictionary ParameterReplacements { get; } = new Dictionary();
+
+ /// Convenience method
+ public void AddParameterReplacement(string key ,string value) => ParameterReplacements.Add(key, value);
+
+ /// If set, truncate each parameter replacement to this many characters. Default 50
+ public int? ParameterMaxSize { get; set; } = 50;
+
+ /// Optional step 2: Replace all illegal characters with this. Default=
+ public string IllegalCharacterReplacements { get; set; }
+
+ /// Generate a valid path for this file or directory
+ public string GetFilename()
+ {
+ var filename = Template;
+
+ foreach (var r in ParameterReplacements)
+ filename = filename.Replace($"<{formatKey(r.Key)}>", formatValue(r.Value));
+
+ return FileUtility.GetValidFilename(filename, IllegalCharacterReplacements);
+ }
+
+ private string formatKey(string key)
+ => key
+ .Replace("<", "")
+ .Replace(">", "");
+
+ private string formatValue(string value)
+ => ParameterMaxSize.HasValue && ParameterMaxSize.Value > 0
+ ? value?.Truncate(ParameterMaxSize.Value)
+ : value;
+ }
+
+}
diff --git a/FileManager/FileUtility.cs b/FileManager/FileUtility.cs
index 9bfcb3fb..c6cdbc88 100644
--- a/FileManager/FileUtility.cs
+++ b/FileManager/FileUtility.cs
@@ -10,70 +10,134 @@ namespace FileManager
{
public static class FileUtility
{
- private const int MAX_FILENAME_LENGTH = 255;
- private const int MAX_DIRECTORY_LENGTH = 247;
+ ///
+ /// "txt" => ".txt"
+ ///
".txt" => ".txt"
+ ///
null or whitespace => ""
+ ///
+ public static string GetStandardizedExtension(string extension)
+ => string.IsNullOrWhiteSpace(extension)
+ ? (extension ?? "")?.Trim()
+ : '.' + extension.Trim('.');
public static string GetValidFilename(string dirFullPath, string filename, string extension, string metadataSuffix)
{
- if (string.IsNullOrWhiteSpace(dirFullPath))
- throw new ArgumentException($"{nameof(dirFullPath)} may not be null or whitespace", nameof(dirFullPath));
+ ArgumentValidator.EnsureNotNullOrWhiteSpace(dirFullPath, nameof(dirFullPath));
+ ArgumentValidator.EnsureNotNullOrWhiteSpace(filename, nameof(filename));
- filename ??= "";
+ var template = $" []";
- // sanitize. omit invalid characters. exception: colon => underscore
- filename = filename.Replace(':', '_');
- filename = PathLib.ToPathSafeString(filename);
+ var fullfilename = Path.Combine(dirFullPath, template + GetStandardizedExtension(extension));
- if (filename.Length > 50)
- filename = filename.Substring(0, 50) + "[...]";
-
- if (!string.IsNullOrWhiteSpace(metadataSuffix))
- filename += $" [{metadataSuffix}]";
-
- // extension is null when this method is used for directory names
- if (!string.IsNullOrWhiteSpace(extension))
- extension = '.' + extension.Trim('.');
-
- // ensure uniqueness
- var fullfilename = Path.Combine(dirFullPath, filename + extension);
- var i = 0;
- while (File.Exists(fullfilename))
- fullfilename = Path.Combine(dirFullPath, filename + $" ({++i})" + extension);
-
- return fullfilename;
+ var fileTemplate = new FileTemplate(fullfilename) { IllegalCharacterReplacements = "_" };
+ fileTemplate.AddParameterReplacement("title", filename);
+ fileTemplate.AddParameterReplacement("id", metadataSuffix);
+ return fileTemplate.GetFilename();
}
public static string GetMultipartFileName(string originalPath, int partsPosition, int partsTotal, string suffix)
{
+ ArgumentValidator.EnsureNotNull(originalPath, nameof(originalPath));
+ ArgumentValidator.EnsureGreaterThan(partsPosition, nameof(partsPosition), 0);
+ ArgumentValidator.EnsureGreaterThan(partsTotal, nameof(partsTotal), 0);
+ if (partsPosition > partsTotal)
+ throw new ArgumentException($"{partsPosition} may not be greater than {partsTotal}");
+
// 1-9 => 1-9
// 10-99 => 01-99
// 100-999 => 001-999
var chapterCountLeadingZeros = partsPosition.ToString().PadLeft(partsTotal.ToString().Length, '0');
- string extension = Path.GetExtension(originalPath);
+ var template = Path.ChangeExtension(originalPath, null) + " - - " + Path.GetExtension(originalPath);
- var filenameBase = $"{Path.GetFileNameWithoutExtension(originalPath)} - {chapterCountLeadingZeros} - {suffix}";
+ var fileTemplate = new FileTemplate(template) { IllegalCharacterReplacements = " " };
+ fileTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
+ fileTemplate.AddParameterReplacement("title", suffix);
- // Replace illegal path characters with spaces
- var filenameBaseSafe = PathLib.ToPathSafeString(filenameBase, " ");
- var fileName = filenameBaseSafe.Truncate(MAX_FILENAME_LENGTH - extension.Length);
- var path = Path.Combine(Path.GetDirectoryName(originalPath), fileName + extension);
- return path;
+ return fileTemplate.GetFilename();
}
- public static string Move(string source, string destination)
+ private const int MAX_FILENAME_LENGTH = 255;
+ private const int MAX_DIRECTORY_LENGTH = 247;
+
+ ///
+ /// Ensure valid file name path:
+ ///
- remove invalid chars
+ ///
- ensure uniqueness
+ ///
- enforce max file length
+ ///
+ public static string GetValidFilename(string path, string illegalCharacterReplacements = "")
{
- // TODO: destination must be valid path. Use: " (#)" when needed
+ ArgumentValidator.EnsureNotNull(path, nameof(path));
+
+ // remove invalid chars
+ path = GetSafePath(path, illegalCharacterReplacements);
+
+ // ensure uniqueness and check lengths
+ var dir = Path.GetDirectoryName(path);
+ dir = dir.Truncate(MAX_DIRECTORY_LENGTH);
+
+ var filename = Path.GetFileNameWithoutExtension(path);
+ var fileStem = Path.Combine(dir, filename);
+
+ var extension = Path.GetExtension(path);
+
+ var fullfilename = fileStem.Truncate(MAX_FILENAME_LENGTH - extension.Length) + extension;
+
+ var i = 0;
+ while (File.Exists(fullfilename))
+ {
+ var increm = $" ({++i})";
+ fullfilename = fileStem.Truncate(MAX_FILENAME_LENGTH - increm.Length - extension.Length) + increm + extension;
+ }
+
+ return fullfilename;
+ }
+
+ // GetInvalidFileNameChars contains everything in GetInvalidPathChars plus ':', '*', '?', '\\', '/'
+
+ /// Use with file name, not full path. Valid path charaters which are invalid file name characters will be replaced: ':', '\\', '/'
+ public static string GetSafeFileName(string str, string illegalCharacterReplacements = "")
+ => string.Join(illegalCharacterReplacements ?? "", str.Split(Path.GetInvalidFileNameChars()));
+
+ /// Use with full path, not file name. Valid path charaters which are invalid file name characters will be retained: '\\', '/'
+ public static string GetSafePath(string path, string illegalCharacterReplacements = "")
+ {
+ ArgumentValidator.EnsureNotNull(path, nameof(path));
+
+ var fixedPath = string
+ .Join(illegalCharacterReplacements ?? "", path.Split(Path.GetInvalidPathChars()))
+ .Replace("*", illegalCharacterReplacements)
+ .Replace("?", illegalCharacterReplacements)
+ .Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
+
+ // replace all colons except within the first 2 chars
+ var builder = new System.Text.StringBuilder();
+ for (var i = 0; i < fixedPath.Length; i++)
+ {
+ var c = fixedPath[i];
+ if (i >= 2 && c == ':')
+ builder.Append(illegalCharacterReplacements);
+ else
+ builder.Append(c);
+ }
+ fixedPath = builder.ToString();
+ return fixedPath;
+ }
+
+ ///
+ /// Move file.
+ ///
- Ensure valid file name path: remove invalid chars, ensure uniqueness, enforce max file length
+ ///
- Perform
+ ///
- Return valid path
+ ///
+ public static string SaferMoveToValidPath(string source, string destination)
+ {
+ destination = GetValidFilename(destination);
SaferMove(source, destination);
return destination;
}
- public static string GetValidFilename(string path)
- {
- // TODO: destination must be valid path. Use: " (#)" when needed
- return path;
- }
-
private static int maxRetryAttempts { get; } = 3;
private static TimeSpan pauseBetweenFailures { get; } = TimeSpan.FromMilliseconds(100);
private static RetryPolicy retryPolicy { get; } =
@@ -81,13 +145,13 @@ namespace FileManager
.Handle()
.WaitAndRetry(maxRetryAttempts, i => pauseBetweenFailures);
- /// Delete file. No error when source does not exist. Retry up to 3 times.
+ /// Delete file. No error when source does not exist. Retry up to 3 times before throwing exception.
public static void SaferDelete(string source)
=> retryPolicy.Execute(() =>
{
try
{
- if (!File.Exists(source))
+ if (File.Exists(source))
{
File.Delete(source);
Serilog.Log.Logger.Information("File successfully deleted", new { source });
@@ -100,22 +164,23 @@ namespace FileManager
}
});
- /// Move file. No error when source does not exist. Retry up to 3 times.
- public static void SaferMove(string source, string target)
+ /// Move file. No error when source does not exist. Retry up to 3 times before throwing exception.
+ public static void SaferMove(string source, string destination)
=> retryPolicy.Execute(() =>
{
try
{
- if (!File.Exists(source))
+ if (File.Exists(source))
{
- SaferDelete(target);
- File.Move(source, target);
- Serilog.Log.Logger.Information("File successfully moved", new { source, target });
+ SaferDelete(destination);
+ Directory.CreateDirectory(Path.GetDirectoryName(destination));
+ File.Move(source, destination);
+ Serilog.Log.Logger.Information("File successfully moved", new { source, destination });
}
}
catch (Exception e)
{
- Serilog.Log.Logger.Error(e, "Failed to move file", new { source, target });
+ Serilog.Log.Logger.Error(e, "Failed to move file", new { source, destination });
throw;
}
});
diff --git a/InternalUtilities/InternalUtilities.csproj b/InternalUtilities/InternalUtilities.csproj
index 67020f0b..fc340968 100644
--- a/InternalUtilities/InternalUtilities.csproj
+++ b/InternalUtilities/InternalUtilities.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/LibationFileManager/AudibleFileStorage.cs b/LibationFileManager/AudibleFileStorage.cs
index 6182eed1..6e6a5be5 100644
--- a/LibationFileManager/AudibleFileStorage.cs
+++ b/LibationFileManager/AudibleFileStorage.cs
@@ -14,7 +14,6 @@ namespace LibationFileManager
#region static
public static string DownloadsInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
public static string DecryptInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
- public static string PdfDirectory => BooksDirectory;
private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage();
public static bool AaxcExists(string productId) => AAXC.Exists(productId);
diff --git a/LibationFileManager/LibationFileManager.csproj b/LibationFileManager/LibationFileManager.csproj
index 9c5e806b..d6498bc0 100644
--- a/LibationFileManager/LibationFileManager.csproj
+++ b/LibationFileManager/LibationFileManager.csproj
@@ -4,6 +4,10 @@
net5.0
+
+
+
+
diff --git a/LibationWinForms/LibationWinForms.csproj b/LibationWinForms/LibationWinForms.csproj
index 615a8b52..b78d85c2 100644
--- a/LibationWinForms/LibationWinForms.csproj
+++ b/LibationWinForms/LibationWinForms.csproj
@@ -29,7 +29,7 @@
-
+