diff --git a/Source/FileManager/FileNamingTemplate.cs b/Source/FileManager/FileNamingTemplate.cs index 203b9acf..49270c3b 100644 --- a/Source/FileManager/FileNamingTemplate.cs +++ b/Source/FileManager/FileNamingTemplate.cs @@ -44,12 +44,20 @@ namespace FileManager var fileNamePart = pathParts[^1]; pathParts.Remove(fileNamePart); + var fileExtension = Path.GetExtension(fileNamePart); + fileNamePart = fileNamePart[..^fileExtension.Length]; + LongPath directory = Path.Join(pathParts.Select(p => replaceFileName(p, paramReplacements, LongPath.MaxFilenameLength)).ToArray()); //If file already exists, GetValidFilename will append " (n)" to the filename. //This could cause the filename length to exceed MaxFilenameLength, so reduce //allowable filename length by 5 chars, allowing for up to 99 duplicates. - return FileUtility.GetValidFilename(Path.Join(directory, replaceFileName(fileNamePart, paramReplacements, LongPath.MaxFilenameLength - 5)), replacements, returnFirstExisting); + return FileUtility + .GetValidFilename( + Path.Join(directory, replaceFileName(fileNamePart, paramReplacements, LongPath.MaxFilenameLength - fileExtension.Length - 5)) + fileExtension, + replacements, + returnFirstExisting + ); } private static string replaceFileName(string filename, Dictionary paramReplacements, int maxFilenameLength) @@ -88,7 +96,7 @@ namespace FileManager //Remove 1 character from the end of the longest filename part until //the total filename is less than max filename length - while (filenameParts.Sum(p => p.Length) > maxFilenameLength) + while (filenameParts.Sum(p => LongPath.GetFilesystemStringLength(p)) > maxFilenameLength) { int maxLength = filenameParts.Max(p => p.Length); var maxEntry = filenameParts.First(p => p.Length == maxLength); 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 a4a467d2..3c10a56f 100644 --- a/Source/FileManager/LongPath.cs +++ b/Source/FileManager/LongPath.cs @@ -10,22 +10,59 @@ 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; - - private const int MAX_PATH = 260; - private const string LONG_PATH_PREFIX = @"\\?\"; - - public string Path { get; init; } - public override string ToString() => Path; - - private static readonly PlatformID PlatformID = Environment.OSVersion.Platform; - + public static readonly int MaxDirectoryLength; + public static readonly int MaxPathLength; + private const int WIN_MAX_PATH = 260; + private const string WIN_LONG_PATH_PREFIX = @"\\?\"; + 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 string Path { get; } + + static LongPath() + { + if (IsWindows) + { + MaxPathLength = short.MaxValue; + MaxDirectoryLength = MaxPathLength - 13; + } + else if (IsOSX) + { + MaxPathLength = 1024; + MaxDirectoryLength = MaxPathLength - MaxFilenameLength; + } + else + { + MaxPathLength = 4096; + MaxDirectoryLength = MaxPathLength - MaxFilenameLength; + } + } + + 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; + } + + //Filename limits on NTFS and FAT filesystems are based on characters, + //but on ext* filesystems they're based on bytes. The ext* filesystems + //don't care about encoding, so how unicode characters are encoded is + ///a choice made by the linux kernel. As best as I can tell, pretty + //much everyone uses UTF-8. + public static int GetFilesystemStringLength(StringBuilder filename) + => LongPath.IsWindows ? + filename.Length + : Encoding.UTF8.GetByteCount(filename.ToString()); + 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 +70,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 +87,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 +100,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 +140,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,13 +154,16 @@ 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; } } + public override string ToString() => Path; + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] private static extern int GetShortPathName([MarshalAs(UnmanagedType.LPWStr)] string path, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder shortPath, int shortPathLength); diff --git a/Source/LibationAvalonia/Libation.desktop b/Source/LibationAvalonia/Libation.desktop index ad1b12f4..0b935ce8 100644 --- a/Source/LibationAvalonia/Libation.desktop +++ b/Source/LibationAvalonia/Libation.desktop @@ -1,6 +1,6 @@ [Desktop Entry] Name=Libation -Exec=Libation +Exec=libation Icon=libation Comment=Liberate your Audiobooks Terminal=false diff --git a/Source/targz2deb.sh b/Source/targz2deb.sh new file mode 100644 index 00000000..d5591f48 --- /dev/null +++ b/Source/targz2deb.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +FILE=$1 + +if [ -z "$FILE" ] +then + echo "This script must be called with a the Libation Linux bin zip file as an argument." + exit +fi + +if [ ! -f "$FILE" ] +then + echo "The file \"$FILE\" does not exist." + exit +fi + +# remove trailing ".tar.gz" +FOLDER_MAIN=${FILE::-7} +echo "Working dir: $FOLDER_MAIN" + +if [[ -d "$FOLDER_MAIN" ]] +then + echo "$FOLDER_MAIN directory already exists, aborting." + exit +fi + +FOLDER_EXEC="$FOLDER_MAIN/usr/lib/libation" +echo "Exec dir: $FOLDER_EXEC" + +FOLDER_ICON="$FOLDER_MAIN/usr/share/icons/hicolor/scalable/apps/" +echo "Icon dir: $FOLDER_ICON" + +FOLDER_DESKTOP="$FOLDER_MAIN/usr/share/applications" +echo "Desktop dir: $FOLDER_DESKTOP" + +FOLDER_DEBIAN="$FOLDER_MAIN/DEBIAN" +echo "Debian dir: $FOLDER_DEBIAN" + +mkdir -p "$FOLDER_EXEC" +mkdir -p "$FOLDER_ICON" +mkdir -p "$FOLDER_DESKTOP" +mkdir -p "$FOLDER_DEBIAN" + +echo "Extracting $FILE to $FOLDER_EXEC..." +tar -xzf ${FILE} -C ${FOLDER_EXEC} + +if [ $? -ne 0 ] + then echo "Error extracting ${FILE}" + exit +fi + +echo "Copying icon..." +cp "$FOLDER_EXEC/glass-with-glow_256.svg" "$FOLDER_ICON/libation.svg" + +echo "Copying desktop file..." +cp "$FOLDER_EXEC/Libation.desktop" "$FOLDER_DESKTOP/Libation.desktop" + +echo "Workaround for desktop file..." +sed -i '/^Exec=Libation/c\Exec=/usr/bin/libation' "$FOLDER_DESKTOP/Libation.desktop" + +echo "Creating pre-install file..." +echo "#!/bin/bash + +# Pre-install script, removes previous installation program files and sym links + +echo \"Removing previously created symlinks...\" + +rm /usr/bin/libation +rm /usr/bin/Libation +rm /usr/bin/hangover +rm /usr/bin/Hangover +rm /usr/bin/libationcli +rm /usr/bin/LibationCli + +echo \"Removing previously installed Libation files...\" + +rm -r /usr/lib/libation +rm -r /usr/lib/Libation + +# making sure it won't stop installation +exit 0 +" >> "$FOLDER_DEBIAN/preinst" + +echo "Creating post-install file..." +echo "#!/bin/bash + +gtk-update-icon-cache -f /usr/share/icons/hicolor/ + +ln -s /usr/lib/libation/Libation /usr/bin/libation +ln -s /usr/lib/libation/Hangover /usr/bin/hangover +ln -s /usr/lib/libation/LibationCli /usr/bin/libationcli + +# workaround until this file is moved to the user's home directory +touch /usr/lib/libation/appsettings.json +chmod 666 /usr/lib/libation/appsettings.json +" >> "$FOLDER_DEBIAN/postinst" + +echo "Creating control file..." +echo "Package: Libation +Version: 8.7.0 +Architecture: all +Essential: no +Priority: optional +Depends: ffmpeg +Maintainer: github.com/rmcrackan +Description: liberate your audiobooks +" >> "$FOLDER_DEBIAN/control" + +echo "Changing permissions for pre- and post-install files..." +chmod +x "$FOLDER_DEBIAN/preinst" +chmod +x "$FOLDER_DEBIAN/postinst" + +echo "Creating .deb file..." +dpkg-deb --build $FOLDER_MAIN + +rm -r "$FOLDER_MAIN" + +echo "Done!"