283 lines
11 KiB
C#
283 lines
11 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using System.Collections.Concurrent;
|
|
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace LightlessSync.Utils
|
|
{
|
|
public static class FileSystemHelper
|
|
{
|
|
public enum FilesystemType
|
|
{
|
|
Unknown = 0,
|
|
NTFS, // Compressable on file level
|
|
Btrfs, // Compressable on file level
|
|
Ext4, // Uncompressable
|
|
Xfs, // Uncompressable
|
|
Apfs, // Compressable on OS
|
|
HfsPlus, // Compressable on OS
|
|
Fat, // Uncompressable
|
|
Exfat, // Uncompressable
|
|
Zfs // Compressable, not on file level
|
|
}
|
|
|
|
private const string _mountPath = "/proc/mounts";
|
|
private const int _defaultBlockSize = 4096;
|
|
private static readonly Dictionary<string, int> _blockSizeCache = new(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly ConcurrentDictionary<string, FilesystemType> _filesystemTypeCache = new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
public static FilesystemType GetFilesystemType(string filePath, bool isWine = false)
|
|
{
|
|
try
|
|
{
|
|
string rootPath;
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && (!IsProbablyWine() || !isWine))
|
|
{
|
|
var info = new FileInfo(filePath);
|
|
var dir = info.Directory ?? new DirectoryInfo(filePath);
|
|
rootPath = dir.Root.FullName;
|
|
}
|
|
else
|
|
{
|
|
rootPath = GetMountPoint(filePath);
|
|
if (string.IsNullOrEmpty(rootPath))
|
|
rootPath = "/";
|
|
}
|
|
|
|
if (_filesystemTypeCache.TryGetValue(rootPath, out var cachedType))
|
|
return cachedType;
|
|
|
|
FilesystemType detected;
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && (!IsProbablyWine() || !isWine))
|
|
{
|
|
var root = new DriveInfo(rootPath);
|
|
var format = root.DriveFormat?.ToUpperInvariant() ?? string.Empty;
|
|
|
|
detected = format switch
|
|
{
|
|
"NTFS" => FilesystemType.NTFS,
|
|
"FAT32" => FilesystemType.Fat,
|
|
"EXFAT" => FilesystemType.Exfat,
|
|
_ => FilesystemType.Unknown
|
|
};
|
|
}
|
|
else
|
|
{
|
|
detected = GetLinuxFilesystemType(filePath);
|
|
}
|
|
|
|
if (isWine || IsProbablyWine())
|
|
{
|
|
switch (detected)
|
|
{
|
|
case FilesystemType.NTFS:
|
|
case FilesystemType.Unknown:
|
|
{
|
|
var linuxDetected = GetLinuxFilesystemType(filePath);
|
|
if (linuxDetected != FilesystemType.Unknown)
|
|
{
|
|
detected = linuxDetected;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
_filesystemTypeCache[rootPath] = detected;
|
|
return detected;
|
|
}
|
|
catch
|
|
{
|
|
return FilesystemType.Unknown;
|
|
}
|
|
}
|
|
|
|
private static string GetMountPoint(string filePath)
|
|
{
|
|
try
|
|
{
|
|
var path = Path.GetFullPath(filePath);
|
|
if (!File.Exists(_mountPath)) return "/";
|
|
var mounts = File.ReadAllLines(_mountPath);
|
|
|
|
string bestMount = "/";
|
|
foreach (var line in mounts)
|
|
{
|
|
var parts = line.Split(' ');
|
|
if (parts.Length < 3) continue;
|
|
var mountPoint = parts[1].Replace("\\040", " ", StringComparison.Ordinal);
|
|
|
|
string normalizedMount;
|
|
try { normalizedMount = Path.GetFullPath(mountPoint); }
|
|
catch { normalizedMount = mountPoint; }
|
|
|
|
if (path.StartsWith(normalizedMount, StringComparison.Ordinal) &&
|
|
normalizedMount.Length > bestMount.Length)
|
|
{
|
|
bestMount = normalizedMount;
|
|
}
|
|
}
|
|
|
|
return bestMount;
|
|
}
|
|
catch
|
|
{
|
|
return "/";
|
|
}
|
|
}
|
|
|
|
public static string GetMountOptionsForPath(string path)
|
|
{
|
|
try
|
|
{
|
|
var fullPath = Path.GetFullPath(path);
|
|
var mounts = File.ReadAllLines("/proc/mounts");
|
|
string bestMount = string.Empty;
|
|
string mountOptions = string.Empty;
|
|
|
|
foreach (var line in mounts)
|
|
{
|
|
var parts = line.Split(' ');
|
|
if (parts.Length < 4) continue;
|
|
var mountPoint = parts[1].Replace("\\040", " ", StringComparison.Ordinal);
|
|
string normalized;
|
|
try { normalized = Path.GetFullPath(mountPoint); }
|
|
catch { normalized = mountPoint; }
|
|
|
|
if (fullPath.StartsWith(normalized, StringComparison.Ordinal) &&
|
|
normalized.Length > bestMount.Length)
|
|
{
|
|
bestMount = normalized;
|
|
mountOptions = parts[3];
|
|
}
|
|
}
|
|
|
|
return mountOptions;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
private static FilesystemType GetLinuxFilesystemType(string filePath)
|
|
{
|
|
try
|
|
{
|
|
var mountPoint = GetMountPoint(filePath);
|
|
var mounts = File.ReadAllLines(_mountPath);
|
|
|
|
foreach (var line in mounts)
|
|
{
|
|
var parts = line.Split(' ');
|
|
if (parts.Length < 3) continue;
|
|
var mount = parts[1].Replace("\\040", " ", StringComparison.Ordinal);
|
|
if (string.Equals(mount, mountPoint, StringComparison.Ordinal))
|
|
{
|
|
var fstype = parts[2].ToLowerInvariant();
|
|
return fstype switch
|
|
{
|
|
"btrfs" => FilesystemType.Btrfs,
|
|
"ext4" => FilesystemType.Ext4,
|
|
"xfs" => FilesystemType.Xfs,
|
|
"zfs" => FilesystemType.Zfs,
|
|
"apfs" => FilesystemType.Apfs,
|
|
"hfsplus" => FilesystemType.HfsPlus,
|
|
_ => FilesystemType.Unknown
|
|
};
|
|
}
|
|
}
|
|
|
|
return FilesystemType.Unknown;
|
|
}
|
|
catch
|
|
{
|
|
return FilesystemType.Unknown;
|
|
}
|
|
}
|
|
|
|
public static int GetBlockSizeForPath(string path, ILogger? logger = null, bool isWine = false)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
return _defaultBlockSize;
|
|
|
|
var fi = new FileInfo(path);
|
|
if (!fi.Exists)
|
|
return _defaultBlockSize;
|
|
|
|
var root = fi.Directory?.Root.FullName.ToLowerInvariant() ?? "/";
|
|
if (_blockSizeCache.TryGetValue(root, out int cached))
|
|
return cached;
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !isWine)
|
|
{
|
|
int result = GetDiskFreeSpaceW(root,
|
|
out uint sectorsPerCluster,
|
|
out uint bytesPerSector,
|
|
out _,
|
|
out _);
|
|
|
|
if (result == 0)
|
|
{
|
|
logger?.LogWarning("Failed to determine block size for {root}", root);
|
|
return _defaultBlockSize;
|
|
}
|
|
|
|
int clusterSize = (int)(sectorsPerCluster * bytesPerSector);
|
|
_blockSizeCache[root] = clusterSize;
|
|
logger?.LogTrace("NTFS cluster size for {root}: {cluster}", root, clusterSize);
|
|
return clusterSize;
|
|
}
|
|
|
|
string realPath = fi.FullName;
|
|
if (isWine && realPath.StartsWith("Z:\\", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
realPath = "/" + realPath.Substring(3).Replace('\\', '/');
|
|
}
|
|
|
|
var psi = new ProcessStartInfo
|
|
{
|
|
FileName = "/bin/bash",
|
|
Arguments = $"-c \"stat -f -c %s '{realPath.Replace("'", "'\\''")}'\"",
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
WorkingDirectory = "/"
|
|
};
|
|
|
|
using var proc = Process.Start(psi);
|
|
string stdout = proc?.StandardOutput.ReadToEnd().Trim() ?? "";
|
|
proc?.WaitForExit();
|
|
|
|
if (int.TryParse(stdout, out int blockSize) && blockSize > 0)
|
|
{
|
|
_blockSizeCache[root] = blockSize;
|
|
logger?.LogTrace("Filesystem block size via stat for {root}: {block}", root, blockSize);
|
|
return blockSize;
|
|
}
|
|
|
|
logger?.LogTrace("stat did not return valid block size for {file}, output: {out}", fi.FullName, stdout);
|
|
_blockSizeCache[root] = _defaultBlockSize;
|
|
return _defaultBlockSize;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger?.LogTrace(ex, "Error determining block size for {path}", path);
|
|
return _defaultBlockSize;
|
|
}
|
|
}
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true, PreserveSig = true)]
|
|
private static extern int GetDiskFreeSpaceW([In, MarshalAs(UnmanagedType.LPWStr)] string lpRootPathName, out uint lpSectorsPerCluster, out uint lpBytesPerSector, out uint lpNumberOfFreeClusters, out uint lpTotalNumberOfClusters);
|
|
|
|
//Extra check on
|
|
public static bool IsProbablyWine() => Environment.GetEnvironmentVariable("WINELOADERNOEXEC") != null || Environment.GetEnvironmentVariable("WINEDLLPATH") != null || Directory.Exists("/proc/self") && File.Exists("/proc/mounts");
|
|
}
|
|
}
|