Refactor valid path/filename. Centralize validaion. Universal templating is one step closer
This commit is contained in:
parent
7720110460
commit
d08962cffa
@ -6,7 +6,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean" Version="0.1.9" />
|
||||
<PackageReference Include="Dinah.Core" Version="2.0.0.1" />
|
||||
<PackageReference Include="Dinah.Core" Version="2.0.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<Version>6.2.6.8</Version>
|
||||
<Version>6.2.6.41</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="1.0.6.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="1.0.7.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
|
||||
|
||||
108
FileManager.Tests/FileTemplateTests.cs
Normal file
108
FileManager.Tests/FileTemplateTests.cs
Normal file
@ -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 = $"<title> [<id>]";
|
||||
|
||||
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) + " - <chapter> - <title>" + Path.GetExtension(originalPath);
|
||||
|
||||
var fileTemplate = new FileTemplate(t) { IllegalCharacterReplacements = " " };
|
||||
fileTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
|
||||
fileTemplate.AddParameterReplacement("title", suffix);
|
||||
|
||||
return fileTemplate.GetFilename();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<ArgumentNullException>(() => 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<ArgumentNullException>(() => 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<ArgumentException>(()
|
||||
=> FileUtility.GetMultipartFileName("foo", -1, 2, "")
|
||||
);
|
||||
[TestMethod]
|
||||
public void zero_partsPosition() => Assert.ThrowsException<ArgumentException>(()
|
||||
=> FileUtility.GetMultipartFileName("foo", 0, 2, "")
|
||||
);
|
||||
|
||||
[TestMethod]
|
||||
public void negative_partsTotal() => Assert.ThrowsException<ArgumentException>(()
|
||||
=> FileUtility.GetMultipartFileName("foo", 2, -1, "")
|
||||
);
|
||||
[TestMethod]
|
||||
public void zero_partsTotal() => Assert.ThrowsException<ArgumentException>(()
|
||||
=> FileUtility.GetMultipartFileName("foo", 2, 0, "")
|
||||
);
|
||||
|
||||
[TestMethod]
|
||||
public void partsPosition_greater_than_partsTotal() => Assert.ThrowsException<ArgumentException>(()
|
||||
=> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="2.0.0.1" />
|
||||
<PackageReference Include="Dinah.Core" Version="2.0.1.1" />
|
||||
<PackageReference Include="Polly" Version="7.2.2" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
52
FileManager/FileTemplate.cs
Normal file
52
FileManager/FileTemplate.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
/// <summary>Get valid filename. Advanced features incl. parameterized template</summary>
|
||||
public class FileTemplate
|
||||
{
|
||||
/// <summary>Proposed full file path. May contain optional html-styled template tags. Eg: <name></summary>
|
||||
public string Template { get; }
|
||||
|
||||
/// <param name="template">Proposed file name with optional html-styled template tags.</param>
|
||||
public FileTemplate(string template) => Template = ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
|
||||
|
||||
/// <summary>Optional step 1: Replace html-styled template tags with parameters. Eg {"name", "Bill Gates"} => /<name>/ => /Bill Gates/</summary>
|
||||
public Dictionary<string, string> ParameterReplacements { get; } = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>Convenience method</summary>
|
||||
public void AddParameterReplacement(string key ,string value) => ParameterReplacements.Add(key, value);
|
||||
|
||||
/// <summary>If set, truncate each parameter replacement to this many characters. Default 50</summary>
|
||||
public int? ParameterMaxSize { get; set; } = 50;
|
||||
|
||||
/// <summary>Optional step 2: Replace all illegal characters with this. Default=<see cref="string.Empty"/></summary>
|
||||
public string IllegalCharacterReplacements { get; set; }
|
||||
|
||||
/// <summary>Generate a valid path for this file or directory</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -10,70 +10,134 @@ namespace FileManager
|
||||
{
|
||||
public static class FileUtility
|
||||
{
|
||||
private const int MAX_FILENAME_LENGTH = 255;
|
||||
private const int MAX_DIRECTORY_LENGTH = 247;
|
||||
/// <summary>
|
||||
/// "txt" => ".txt"
|
||||
/// <br />".txt" => ".txt"
|
||||
/// <br />null or whitespace => ""
|
||||
/// </summary>
|
||||
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 = $"<title> [<id>]";
|
||||
|
||||
// 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) + " - <chapter> - <title>" + 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;
|
||||
|
||||
/// <summary>
|
||||
/// Ensure valid file name path:
|
||||
/// <br/>- remove invalid chars
|
||||
/// <br/>- ensure uniqueness
|
||||
/// <br/>- enforce max file length
|
||||
/// </summary>
|
||||
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 ':', '*', '?', '\\', '/'
|
||||
|
||||
/// <summary>Use with file name, not full path. Valid path charaters which are invalid file name characters will be replaced: ':', '\\', '/'</summary>
|
||||
public static string GetSafeFileName(string str, string illegalCharacterReplacements = "")
|
||||
=> string.Join(illegalCharacterReplacements ?? "", str.Split(Path.GetInvalidFileNameChars()));
|
||||
|
||||
/// <summary>Use with full path, not file name. Valid path charaters which are invalid file name characters will be retained: '\\', '/'</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move file.
|
||||
/// <br/>- Ensure valid file name path: remove invalid chars, ensure uniqueness, enforce max file length
|
||||
/// <br/>- Perform <see cref="SaferMove"/>
|
||||
/// <br/>- Return valid path
|
||||
/// </summary>
|
||||
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<Exception>()
|
||||
.WaitAndRetry(maxRetryAttempts, i => pauseBetweenFailures);
|
||||
|
||||
/// <summary>Delete file. No error when source does not exist. Retry up to 3 times.</summary>
|
||||
/// <summary>Delete file. No error when source does not exist. Retry up to 3 times before throwing exception.</summary>
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
/// <summary>Move file. No error when source does not exist. Retry up to 3 times.</summary>
|
||||
public static void SaferMove(string source, string target)
|
||||
/// <summary>Move file. No error when source does not exist. Retry up to 3 times before throwing exception.</summary>
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="2.3.0.1" />
|
||||
<PackageReference Include="AudibleApi" Version="2.3.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="2.0.0.1" />
|
||||
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="2.0.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user