Smart handling of filename limitations cross platform
Automatically determine if filename lengths in the Books directory are limited to 255 UTF-16 characters (NTFS) or 255 UTF-8 bytes (pretty much every other file system) (#1260) In non-Windows environments, determine if the Books directory supports filenames containing characters which are illegal in Windows environments (<>|:*?). If it doesn't, then ensure those characters are included in the user's ReplacementCharacters settings (#1258).
This commit is contained in:
parent
7024bbf823
commit
ae012548bd
74
Source/FileManager/FileSystemTest.cs
Normal file
74
Source/FileManager/FileSystemTest.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
public static class FileSystemTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Additional characters which are illegal for filenames in Windows environments.
|
||||
/// Double quotes and slashes are already illegal filename characters on all platforms,
|
||||
/// so they are not included here.
|
||||
/// </summary>
|
||||
public static string AdditionalInvalidWindowsFilenameCharacters { get; } = "<>|:*?";
|
||||
|
||||
/// <summary>
|
||||
/// Test if the directory supports filenames with characters that are invalid on Windows (:, *, ?, <, >, |).
|
||||
/// </summary>
|
||||
public static bool CanWriteWindowsInvalidChars(LongPath directoryName)
|
||||
{
|
||||
var testFile = Path.Combine(directoryName, AdditionalInvalidWindowsFilenameCharacters + Guid.NewGuid().ToString());
|
||||
return CanWriteFile(testFile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if the directory supports filenames with 255 unicode characters.
|
||||
/// </summary>
|
||||
public static bool CanWrite255UnicodeChars(LongPath directoryName)
|
||||
{
|
||||
const char unicodeChar = 'ü';
|
||||
var testFileName = new string(unicodeChar, 255);
|
||||
var testFile = Path.Combine(directoryName, testFileName);
|
||||
return CanWriteFile(testFile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if a directory has write access by attempting to create an empty file in it.
|
||||
/// <para/>Returns true even if the temporary file can not be deleted.
|
||||
/// </summary>
|
||||
public static bool CanWriteDirectory(LongPath directoryName)
|
||||
{
|
||||
if (!Directory.Exists(directoryName))
|
||||
return false;
|
||||
|
||||
Serilog.Log.Logger.Debug("Testing write permissions for directory: {@DirectoryName}", directoryName);
|
||||
var testFilePath = Path.Combine(directoryName, Guid.NewGuid().ToString());
|
||||
return CanWriteFile(testFilePath);
|
||||
}
|
||||
|
||||
private static bool CanWriteFile(LongPath filename)
|
||||
{
|
||||
try
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Testing ability to write filename: {@filename}", filename);
|
||||
File.WriteAllBytes(filename, []);
|
||||
Serilog.Log.Logger.Debug("Deleting test file after successful write: {@filename}", filename);
|
||||
try
|
||||
{
|
||||
FileUtility.SaferDelete(filename);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//An error deleting the file doesn't constitute a write failure.
|
||||
Serilog.Log.Logger.Debug(ex, "Error deleting test file: {@filename}", filename);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Debug(ex, "Error writing test file: {@filename}", filename);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -43,6 +43,38 @@ namespace LibationAvalonia.Views
|
||||
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(ViewModel.ShowAccountsAsync), Gesture = new KeyGesture(Key.A, KeyModifiers.Control | KeyModifiers.Shift) });
|
||||
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(ViewModel.ExportLibraryAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Control) });
|
||||
}
|
||||
|
||||
Configuration.Instance.PropertyChanged += Settings_PropertyChanged;
|
||||
Settings_PropertyChanged(this, null);
|
||||
}
|
||||
|
||||
[Dinah.Core.PropertyChangeFilter(nameof(Configuration.Books))]
|
||||
private void Settings_PropertyChanged(object sender, Dinah.Core.PropertyChangedEventArgsEx e)
|
||||
{
|
||||
if (!Configuration.IsWindows && !Configuration.Instance.BooksCanWriteWindowsInvalidChars)
|
||||
{
|
||||
//The books directory does not support filenames with windows' invalid characters.
|
||||
//Ensure that the ReplacementCharacters configuration has replacements for all invalid characters.
|
||||
//We can't rely on the "other invalid characters" replacement because that is only used by
|
||||
//ReplacementCharacters for platform-specific illegal characters, whereas for the Books directory
|
||||
//we are concerned with the ultimate destination directory's capabilities.
|
||||
var defaults = ReplacementCharacters.Default(true).Replacements;
|
||||
var replacements = Configuration.Instance.ReplacementCharacters.Replacements.ToList();
|
||||
bool changed = false;
|
||||
foreach (var c in FileSystemTest.AdditionalInvalidWindowsFilenameCharacters)
|
||||
{
|
||||
if (!replacements.Any(r => r.CharacterToReplace == c))
|
||||
{
|
||||
var replacement = defaults.FirstOrDefault(r => r.CharacterToReplace == c) ?? defaults[0];
|
||||
replacements.Add(replacement);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed)
|
||||
{
|
||||
Configuration.Instance.ReplacementCharacters = new ReplacementCharacters { Replacements = replacements };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AudibleApiStorage_LoadError(object sender, AccountSettingsLoadErrorEventArgs e)
|
||||
|
||||
@ -111,7 +111,34 @@ namespace LibationFileManager
|
||||
public bool BetaOptIn { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
[Description("Location for book storage. Includes destination of newly liberated books")]
|
||||
public LongPath? Books { get => GetString(); set => SetString(value); }
|
||||
public LongPath? Books {
|
||||
get => GetString();
|
||||
set
|
||||
{
|
||||
if (value != Books)
|
||||
{
|
||||
OnPropertyChanging(nameof(Books), Books, value);
|
||||
Settings.SetString(nameof(Books), value);
|
||||
m_BooksCanWrite255UnicodeChars = null;
|
||||
m_BooksCanWriteWindowsInvalidChars = null;
|
||||
OnPropertyChanged(nameof(Books), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool? m_BooksCanWrite255UnicodeChars;
|
||||
private bool? m_BooksCanWriteWindowsInvalidChars;
|
||||
/// <summary>
|
||||
/// True if the Books directory can be written to with 255 unicode character filenames
|
||||
/// <para/> Does not persist. Check and set this value at runtime and whenever Books is changed.
|
||||
/// </summary>
|
||||
public bool BooksCanWrite255UnicodeChars => m_BooksCanWrite255UnicodeChars ??= FileSystemTest.CanWrite255UnicodeChars(Books);
|
||||
/// <summary>
|
||||
/// True if the Books directory can be written to with filenames containing characters invalid on Windows (:, *, ?, <, >, |)
|
||||
/// <para/> Always false on Windows platforms.
|
||||
/// <para/> Does not persist. Check and set this value at runtime and whenever Books is changed.
|
||||
/// </summary>
|
||||
public bool BooksCanWriteWindowsInvalidChars => !IsWindows && (m_BooksCanWriteWindowsInvalidChars ??= FileSystemTest.CanWriteWindowsInvalidChars(Books));
|
||||
|
||||
[Description("Overwrite existing files if they already exist?")]
|
||||
public bool OverwriteExisting { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
@ -157,7 +157,7 @@ namespace LibationFileManager.Templates
|
||||
var maxFilenameLength = LongPath.MaxFilenameLength -
|
||||
(i < pathParts.Count - 1 || string.IsNullOrEmpty(fileExtension) ? 0 : fileExtension.Length + 5);
|
||||
|
||||
while (part.Sum(LongPath.GetFilesystemStringLength) > maxFilenameLength)
|
||||
while (part.Sum(GetFilenameLength) > maxFilenameLength)
|
||||
{
|
||||
int maxLength = part.Max(p => p.Length);
|
||||
var maxEntry = part.First(p => p.Length == maxLength);
|
||||
@ -173,6 +173,10 @@ namespace LibationFileManager.Templates
|
||||
return FileUtility.GetValidFilename(fullPath, replacements, fileExtension, returnFirstExisting);
|
||||
}
|
||||
|
||||
private static int GetFilenameLength(string filename)
|
||||
=> Configuration.Instance.BooksCanWrite255UnicodeChars ? filename.Length
|
||||
: System.Text.Encoding.UTF8.GetByteCount(filename);
|
||||
|
||||
/// <summary>
|
||||
/// Organize template parts into directories. Any Extra slashes will be
|
||||
/// returned as empty directories and are taken care of by Path.Combine()
|
||||
|
||||
@ -23,6 +23,15 @@ namespace TemplatesTests
|
||||
|
||||
public static class Shared
|
||||
{
|
||||
[System.Runtime.CompilerServices.ModuleInitializer]
|
||||
public static void Init()
|
||||
{
|
||||
var thisDir = Path.GetDirectoryName(Environment.ProcessPath);
|
||||
LibationFileManager.Configuration.SetLibationFiles(thisDir);
|
||||
if (!LibationFileManager.Configuration.Instance.LibationSettingsAreValid)
|
||||
LibationFileManager.Configuration.Instance.Books = Path.Combine(thisDir, "Books");
|
||||
}
|
||||
|
||||
public static LibraryBookDto GetLibraryBook()
|
||||
=> GetLibraryBook([new SeriesDto("Sherlock Holmes", "1", "B08376S3R2")]);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user