From f822a23daad730e66944894ca9a8b4c55f8f5a18 Mon Sep 17 00:00:00 2001 From: = Date: Thu, 29 Dec 2022 15:29:18 -0700 Subject: [PATCH] Linux and OSX directory length limits --- Source/FileManager/FileNamingTemplate.cs | 2 +- Source/FileManager/FileUtility.cs | 23 +++++++-- Source/FileManager/LongPath.cs | 65 +++++++++++++++++------- 3 files changed, 67 insertions(+), 23 deletions(-) diff --git a/Source/FileManager/FileNamingTemplate.cs b/Source/FileManager/FileNamingTemplate.cs index ee8446ed..d951d8a0 100644 --- a/Source/FileManager/FileNamingTemplate.cs +++ b/Source/FileManager/FileNamingTemplate.cs @@ -68,7 +68,7 @@ namespace FileManager ///a choice made by the linux kernel. As best as I can tell, pretty //much everyone uses UTF-8. int getFilesystemStringLength(StringBuilder str) - => LongPath.PlatformID is PlatformID.Win32NT ? + => LongPath.IsWindows ? str.Length : Encoding.UTF8.GetByteCount(str.ToString()); diff --git a/Source/FileManager/FileUtility.cs b/Source/FileManager/FileUtility.cs index 03c4f872..5efe7c69 100644 --- a/Source/FileManager/FileUtility.cs +++ b/Source/FileManager/FileUtility.cs @@ -6,11 +6,14 @@ using System.Text.RegularExpressions; using Dinah.Core; using Polly; using Polly.Retry; +using Dinah.Core.Collections.Generic; namespace FileManager { public static class FileUtility { + + /// /// "txt" => ".txt" ///
".txt" => ".txt" @@ -55,15 +58,15 @@ namespace FileManager // ensure uniqueness and check lengths var dir = Path.GetDirectoryName(path); - dir = dir?.Truncate(LongPath.MaxDirectoryLength) ?? string.Empty; + dir = dir?.TruncateFilename(LongPath.MaxDirectoryLength) ?? string.Empty; var extension = Path.GetExtension(path); - var filename = Path.GetFileNameWithoutExtension(path).Truncate(LongPath.MaxFilenameLength - extension.Length); + var filename = Path.GetFileNameWithoutExtension(path).TruncateFilename(LongPath.MaxFilenameLength - extension.Length); var fileStem = Path.Combine(dir, filename); - var fullfilename = fileStem.Truncate(LongPath.MaxPathLength - extension.Length) + extension; + var fullfilename = fileStem.TruncateFilename(LongPath.MaxPathLength - extension.Length) + extension; fullfilename = removeInvalidWhitespace(fullfilename); @@ -71,7 +74,7 @@ namespace FileManager while (File.Exists(fullfilename) && !returnFirstExisting) { var increm = $" ({++i})"; - fullfilename = fileStem.Truncate(LongPath.MaxPathLength - increm.Length - extension.Length) + increm + extension; + fullfilename = fileStem.TruncateFilename(LongPath.MaxPathLength - increm.Length - extension.Length) + increm + extension; } return fullfilename; @@ -129,6 +132,18 @@ namespace FileManager public static string RemoveLastCharacter(this string str) => string.IsNullOrEmpty(str) ? str : str[..^1]; + public static string TruncateFilename(this string filenameStr, int limit) + { + if (LongPath.IsWindows) return filenameStr.Truncate(limit); + + int index = filenameStr.Length; + + while (index > 0 && System.Text.Encoding.UTF8.GetByteCount(filenameStr, 0, index) > limit) + index--; + + return filenameStr[..index]; + } + /// /// Move file. ///
- Ensure valid file name path: remove invalid chars, ensure uniqueness, enforce max file length diff --git a/Source/FileManager/LongPath.cs b/Source/FileManager/LongPath.cs index a5302fe0..11002cd7 100644 --- a/Source/FileManager/LongPath.cs +++ b/Source/FileManager/LongPath.cs @@ -10,22 +10,51 @@ namespace FileManager { //https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd - public const int MaxDirectoryLength = MaxPathLength - 13; - public const int MaxPathLength = short.MaxValue; public const int MaxFilenameLength = 255; + public static readonly int MaxDirectoryLength; + public static readonly int MaxPathLength; - private const int MAX_PATH = 260; - private const string LONG_PATH_PREFIX = @"\\?\"; + static LongPath() + { + if (IsWindows) + { + MaxPathLength = short.MaxValue; + MaxDirectoryLength = MaxPathLength - 13; + } + else if (IsOSX) + { + MaxPathLength = 1024; + MaxDirectoryLength = MaxPathLength - MaxFilenameLength; + } + else + { + MaxPathLength = 4096; + MaxDirectoryLength = MaxPathLength - MaxFilenameLength; + } + } - public string Path { get; init; } + private const int WIN_MAX_PATH = 260; + private const string WIN_LONG_PATH_PREFIX = @"\\?\"; + + private LongPath(string path) + { + if (IsWindows && path.Length > MaxPathLength) + throw new System.IO.PathTooLongException($"Path exceeds {MaxPathLength} character limit. ({path})"); + if ((!IsWindows && Encoding.UTF8.GetByteCount(path) > MaxPathLength)) + throw new System.IO.PathTooLongException($"Path exceeds {MaxPathLength} byte limit. ({path})"); + + Path = path; + } + public string Path { get; } public override string ToString() => Path; - internal static readonly PlatformID PlatformID = Environment.OSVersion.Platform; - + internal static readonly bool IsWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + internal static readonly bool IsLinux = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + internal static readonly bool IsOSX = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.OSX); public static implicit operator LongPath(string path) { - if (PlatformID is PlatformID.Unix) return new LongPath { Path = path }; + if (!IsWindows) return new LongPath(path); if (path is null) return null; @@ -33,15 +62,15 @@ namespace FileManager //the name to an NT-style name, except when using the "\\?\" prefix path = path.Replace(System.IO.Path.AltDirectorySeparatorChar, System.IO.Path.DirectorySeparatorChar); - if (path.StartsWith(LONG_PATH_PREFIX)) - return new LongPath { Path = path }; + if (path.StartsWith(WIN_LONG_PATH_PREFIX)) + return new LongPath(path); else if ((path.Length > 2 && path[1] == ':') || path.StartsWith(@"UNC\")) - return new LongPath { Path = LONG_PATH_PREFIX + path }; + return new LongPath(WIN_LONG_PATH_PREFIX + path); else if (path.StartsWith(@"\\")) //The "\\?\" prefix can also be used with paths constructed according to the //universal naming convention (UNC). To specify such a path using UNC, use //the "\\?\UNC\" prefix. - return new LongPath { Path = LONG_PATH_PREFIX + @"UNC\" + path.Substring(2) }; + return new LongPath(WIN_LONG_PATH_PREFIX + @"UNC\" + path.Substring(2)); else { //These prefixes are not used as part of the path itself. They indicate that @@ -50,9 +79,9 @@ namespace FileManager //a period to represent the current directory, or double dots to represent the //parent directory. Because you cannot use the "\\?\" prefix with a relative //path, relative paths are always limited to a total of MAX_PATH characters. - if (path.Length > MAX_PATH) + if (path.Length > WIN_MAX_PATH) throw new System.IO.PathTooLongException(); - return new LongPath { Path = path }; + return new LongPath(path); } } @@ -63,7 +92,7 @@ namespace FileManager { get { - if (PlatformID is PlatformID.Unix) return Path; + if (!IsWindows) return Path; //Short Path names are useful for navigating to the file in windows explorer, //which will not recognize paths longer than MAX_PATH. Short path names are not @@ -103,7 +132,7 @@ namespace FileManager { get { - if (PlatformID is PlatformID.Unix) return Path; + if (!IsWindows) return Path; if (Path is null) return null; StringBuilder longPathBuffer = new(MaxPathLength); @@ -117,9 +146,9 @@ namespace FileManager { get { - if (PlatformID is PlatformID.Unix) return Path; + if (!IsWindows) return Path; return - Path?.StartsWith(LONG_PATH_PREFIX) == true ? Path.Remove(0, LONG_PATH_PREFIX.Length) + Path?.StartsWith(WIN_LONG_PATH_PREFIX) == true ? Path.Remove(0, WIN_LONG_PATH_PREFIX.Length) :Path; } }