Merge pull request #431 from Mbucari/master

Fix file naming template on unix systems (#430)
This commit is contained in:
rmcrackan 2022-12-30 11:05:09 -05:00 committed by GitHub
commit a0158db37e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 210 additions and 29 deletions

View File

@ -44,12 +44,20 @@ namespace FileManager
var fileNamePart = pathParts[^1]; var fileNamePart = pathParts[^1];
pathParts.Remove(fileNamePart); 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()); LongPath directory = Path.Join(pathParts.Select(p => replaceFileName(p, paramReplacements, LongPath.MaxFilenameLength)).ToArray());
//If file already exists, GetValidFilename will append " (n)" to the filename. //If file already exists, GetValidFilename will append " (n)" to the filename.
//This could cause the filename length to exceed MaxFilenameLength, so reduce //This could cause the filename length to exceed MaxFilenameLength, so reduce
//allowable filename length by 5 chars, allowing for up to 99 duplicates. //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<string,string> paramReplacements, int maxFilenameLength) private static string replaceFileName(string filename, Dictionary<string,string> paramReplacements, int maxFilenameLength)
@ -88,7 +96,7 @@ namespace FileManager
//Remove 1 character from the end of the longest filename part until //Remove 1 character from the end of the longest filename part until
//the total filename is less than max filename length //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); int maxLength = filenameParts.Max(p => p.Length);
var maxEntry = filenameParts.First(p => p.Length == maxLength); var maxEntry = filenameParts.First(p => p.Length == maxLength);

View File

@ -6,11 +6,14 @@ using System.Text.RegularExpressions;
using Dinah.Core; using Dinah.Core;
using Polly; using Polly;
using Polly.Retry; using Polly.Retry;
using Dinah.Core.Collections.Generic;
namespace FileManager namespace FileManager
{ {
public static class FileUtility public static class FileUtility
{ {
/// <summary> /// <summary>
/// "txt" => ".txt" /// "txt" => ".txt"
/// <br />".txt" => ".txt" /// <br />".txt" => ".txt"
@ -55,15 +58,15 @@ namespace FileManager
// ensure uniqueness and check lengths // ensure uniqueness and check lengths
var dir = Path.GetDirectoryName(path); 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 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 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); fullfilename = removeInvalidWhitespace(fullfilename);
@ -71,7 +74,7 @@ namespace FileManager
while (File.Exists(fullfilename) && !returnFirstExisting) while (File.Exists(fullfilename) && !returnFirstExisting)
{ {
var increm = $" ({++i})"; 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; return fullfilename;
@ -129,6 +132,18 @@ namespace FileManager
public static string RemoveLastCharacter(this string str) => string.IsNullOrEmpty(str) ? str : str[..^1]; 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];
}
/// <summary> /// <summary>
/// Move file. /// Move file.
/// <br/>- Ensure valid file name path: remove invalid chars, ensure uniqueness, enforce max file length /// <br/>- Ensure valid file name path: remove invalid chars, ensure uniqueness, enforce max file length

View File

@ -10,22 +10,59 @@ namespace FileManager
{ {
//https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd //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 const int MaxFilenameLength = 255;
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);
private const int MAX_PATH = 260; public string Path { get; }
private const string LONG_PATH_PREFIX = @"\\?\";
public string Path { get; init; } static LongPath()
public override string ToString() => Path; {
if (IsWindows)
{
MaxPathLength = short.MaxValue;
MaxDirectoryLength = MaxPathLength - 13;
}
else if (IsOSX)
{
MaxPathLength = 1024;
MaxDirectoryLength = MaxPathLength - MaxFilenameLength;
}
else
{
MaxPathLength = 4096;
MaxDirectoryLength = MaxPathLength - MaxFilenameLength;
}
}
private static readonly PlatformID PlatformID = Environment.OSVersion.Platform; 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) 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; if (path is null) return null;
@ -33,15 +70,15 @@ namespace FileManager
//the name to an NT-style name, except when using the "\\?\" prefix //the name to an NT-style name, except when using the "\\?\" prefix
path = path.Replace(System.IO.Path.AltDirectorySeparatorChar, System.IO.Path.DirectorySeparatorChar); path = path.Replace(System.IO.Path.AltDirectorySeparatorChar, System.IO.Path.DirectorySeparatorChar);
if (path.StartsWith(LONG_PATH_PREFIX)) if (path.StartsWith(WIN_LONG_PATH_PREFIX))
return new LongPath { Path = path }; return new LongPath(path);
else if ((path.Length > 2 && path[1] == ':') || path.StartsWith(@"UNC\")) 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(@"\\")) else if (path.StartsWith(@"\\"))
//The "\\?\" prefix can also be used with paths constructed according to the //The "\\?\" prefix can also be used with paths constructed according to the
//universal naming convention (UNC). To specify such a path using UNC, use //universal naming convention (UNC). To specify such a path using UNC, use
//the "\\?\UNC\" prefix. //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 else
{ {
//These prefixes are not used as part of the path itself. They indicate that //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 //a period to represent the current directory, or double dots to represent the
//parent directory. Because you cannot use the "\\?\" prefix with a relative //parent directory. Because you cannot use the "\\?\" prefix with a relative
//path, relative paths are always limited to a total of MAX_PATH characters. //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(); throw new System.IO.PathTooLongException();
return new LongPath { Path = path }; return new LongPath(path);
} }
} }
@ -63,7 +100,7 @@ namespace FileManager
{ {
get 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, //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 //which will not recognize paths longer than MAX_PATH. Short path names are not
@ -103,7 +140,7 @@ namespace FileManager
{ {
get get
{ {
if (PlatformID is PlatformID.Unix) return Path; if (!IsWindows) return Path;
if (Path is null) return null; if (Path is null) return null;
StringBuilder longPathBuffer = new(MaxPathLength); StringBuilder longPathBuffer = new(MaxPathLength);
@ -117,13 +154,16 @@ namespace FileManager
{ {
get get
{ {
if (PlatformID is PlatformID.Unix) return Path; if (!IsWindows) return Path;
return 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; :Path;
} }
} }
public override string ToString() => Path;
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)] [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern int GetShortPathName([MarshalAs(UnmanagedType.LPWStr)] string path, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder shortPath, int shortPathLength); private static extern int GetShortPathName([MarshalAs(UnmanagedType.LPWStr)] string path, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder shortPath, int shortPathLength);

View File

@ -1,6 +1,6 @@
[Desktop Entry] [Desktop Entry]
Name=Libation Name=Libation
Exec=Libation Exec=libation
Icon=libation Icon=libation
Comment=Liberate your Audiobooks Comment=Liberate your Audiobooks
Terminal=false Terminal=false

118
Source/targz2deb.sh Normal file
View File

@ -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!"