Files
SteamEmuUtility/Common/FileSystem.cs

797 lines
25 KiB
C#

using Playnite.SDK;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace PluginsCommon
{
// Based on https://github.com/JosefNemec/Playnite
public enum FileSystemItem
{
File,
Directory
}
public static partial class FileSystem
{
[DllImport("kernel32.dll")]
static extern uint GetCompressedFileSizeW([In, MarshalAs(UnmanagedType.LPWStr)] string lpFileName,
[Out, MarshalAs(UnmanagedType.U4)] out uint lpFileSizeHigh);
[DllImport("kernel32.dll", SetLastError = true, PreserveSig = true)]
static extern int GetDiskFreeSpaceW([In, MarshalAs(UnmanagedType.LPWStr)] string lpRootPathName,
out uint lpSectorsPerCluster, out uint lpBytesPerSector, out uint lpNumberOfFreeClusters,
out uint lpTotalNumberOfClusters);
private static ILogger logger = LogManager.GetLogger();
private const string longPathPrefix = @"\\?\";
private const string longPathUncPrefix = @"\\?\UNC\";
/// <summary>
/// Determines the architecture type of the specified executable file.
/// </summary>
/// <param name="exePath">The path to the executable file.</param>
/// <returns>
/// 64/32/0.
/// NOTE
/// 0 means Unknown/null/error.
/// </returns>
public static string GetArchitectureType(string exePath)
{
try
{
exePath = FixPathLength(exePath);
using (FileStream fs = new FileStream(exePath, FileMode.Open, FileAccess.Read))
{
using (BinaryReader br = new BinaryReader(fs))
{
fs.Seek(0x3C, SeekOrigin.Begin); // Move to the PE header offset
int peOffset = br.ReadInt32(); // Read the PE header offset
fs.Seek(peOffset + 4, SeekOrigin.Begin); // Move to the PE signature offset
ushort peSignature = br.ReadUInt16(); // Read the PE signature
if (peSignature == 0x014C) // PE32 (32-bit)
{
return "32"; // 32-bit executable
}
else if (peSignature == 0x8664) // PE32+ (64-bit)
{
return "64"; // 64-bit executable
}
else
{
return "0"; // Unknown architecture
}
}
}
}
catch
{
return "0"; // Error occurred
}
}
public static Process DeleteSymbolicLink(string linkPath, string targetPath)
{
var targetdirectory = FixPathLength(targetPath);
var linkdirectory = FixPathLength(linkPath);
//var linkdirectoryparent = Path.GetDirectoryName(linkdirectory);
ProcessStartInfo psi = new ProcessStartInfo("cmd.exe")
{
RedirectStandardInput = true,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
Process process = new Process { StartInfo = psi };
process.Start();
using (StreamWriter sw = process.StandardInput)
{
if (sw.BaseStream.CanWrite)
{
sw.WriteLine($"mklink /d \"{linkdirectory}\" \"{targetdirectory}\"");
}
}
return process;
}
/// <summary>
/// Create a <c>Symlink</c> and return false if it's failed.
/// </summary>
/// <param name="linkPath">the path that will be created</param>
/// <param name="targetPath">the path (relative or absolute) that the new symbolic link refers to (the source folder that want to link)</param>
public static bool CreateSymbolicLink(string linkPath, string targetPath)
{
var targetdirectory = FixPathLength(targetPath);
var linkdirectory = FixPathLength(linkPath);
var linkdirectoryparent = Path.GetDirectoryName(linkdirectory);
if (!DirectoryExists(linkdirectoryparent))
{
CreateDirectory(linkdirectoryparent);
}
// Run mklink command
ProcessStartInfo psi = new ProcessStartInfo("cmd.exe")
{
RedirectStandardInput = true,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
Process process = new Process { StartInfo = psi };
process.Start();
using (StreamWriter sw = process.StandardInput)
{
if (sw.BaseStream.CanWrite)
{
sw.WriteLine($"mklink /d \"{linkdirectory}\" \"{targetdirectory}\"");
}
}
process.WaitForExit();
string errorMessage = process.StandardError.ReadToEnd();
if (!errorMessage.IsNullOrWhiteSpace())
{
return false;
}
return true;
}
private static Process DeleteSymbolicLink(string path)
{
return ProcessCommon.ProcessUtilities.StartProcessHidden("cmd.exe", $"/c rd \"{path}\"");
}
public static bool IsSymbolicLink(string path)
{
var directory = FixPathLength(path);
DirectoryInfo directoryInfo = new DirectoryInfo(directory);
// Check if the directory attributes include ReparsePoint (symbolic link)
return (directoryInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
}
public static void TryWriteText(string path, object content)
{
var directory = FixPathLength(path);
// Check if the path has a file extension
if (!string.IsNullOrEmpty(Path.GetExtension(directory)))
{
// If it has an extension, it's a file path
// Ensure the directory exists before writing the file
string directoryPath = Path.GetDirectoryName(directory);
if (!Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
}
try
{
switch (content)
{
case string singleText:
File.WriteAllText(path, singleText);
break;
case List<string> textList:
File.WriteAllLines(path, textList);
break;
}
}
catch { }
}
public static void CreateDirectory(string path)
{
CreateDirectory(path, false);
}
public static void CreateDirectory(string path, bool clean)
{
var directory = FixPathLength(path);
if (string.IsNullOrEmpty(directory))
{
return;
}
if (Directory.Exists(directory))
{
if (clean)
{
Directory.Delete(directory, true);
}
else
{
return;
}
}
Directory.CreateDirectory(directory);
}
public static void PrepareSaveFile(string path)
{
path = FixPathLength(path);
CreateDirectory(Path.GetDirectoryName(path));
DeleteFile(path, true);
}
public static bool IsDirectoryEmpty(string path)
{
path = FixPathLength(path);
if (Directory.Exists(path))
{
return !Directory.EnumerateFileSystemEntries(path).Any();
}
else
{
return true;
}
}
public static void DeleteFile(string path, bool includeReadonly = false)
{
path = FixPathLength(path);
if (!File.Exists(path))
{
return;
}
if (includeReadonly)
{
var attr = File.GetAttributes(path);
if ((attr & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
{
File.SetAttributes(path, attr ^ FileAttributes.ReadOnly);
}
}
File.Delete(path);
}
public static void CreateFile(string path, bool createDirectory = false)
{
if (createDirectory)
{
string dir = Path.GetDirectoryName(path);
Directory.CreateDirectory(dir);
}
path = FixPathLength(path);
File.Create(path).Dispose();
}
public static bool CopyFile(string sourcePath, string targetPath, bool overwrite = true)
{
sourcePath = FixPathLength(sourcePath);
targetPath = FixPathLength(targetPath);
try
{
logger.Debug($"Copying file {sourcePath} to {targetPath}");
PrepareSaveFile(targetPath);
File.Copy(sourcePath, targetPath, overwrite);
return true;
}
catch (Exception e)
{
logger.Error(e, $"Error copying file {sourcePath} to {targetPath}");
return false;
}
}
public static bool MoveFile(string sourcePath, string targetPath)
{
sourcePath = FixPathLength(sourcePath);
targetPath = FixPathLength(targetPath);
logger.Debug($"Moving file {sourcePath} to {targetPath}");
if (sourcePath.Equals(targetPath, StringComparison.OrdinalIgnoreCase))
{
logger.Debug($"Source path and target path are the same: {sourcePath}");
return false;
}
if (!File.Exists(sourcePath))
{
logger.Debug($"Source doesn't exists: {sourcePath}");
return false;
}
var targetDir = Path.GetDirectoryName(targetPath);
if (!Directory.Exists(targetDir))
{
Directory.CreateDirectory(targetDir);
}
else if (File.Exists(targetPath))
{
File.Delete(targetPath);
}
File.Move(sourcePath, targetPath);
return true;
}
public static void DeleteDirectory(string path)
{
path = FixPathLength(path);
if (Directory.Exists(path))
{
if (IsSymbolicLink(path))
{
DeleteSymbolicLink(path).WaitForExit(500);
return;
}
Directory.Delete(path, true);
}
}
public static void ClearDirectory(string path)
{
path = FixPathLength(path);
if (!Directory.Exists(path))
{
return;
}
DirectoryInfo dir = new DirectoryInfo(path);
foreach (FileInfo file in dir.GetFiles())
{
DeleteFile(file.FullName);
}
foreach (DirectoryInfo directory in dir.GetDirectories())
{
ClearDirectory(directory.FullName);
DeleteDirectory(directory.FullName);
}
}
public static void DeleteDirectory(string path, bool includeReadonly)
{
path = FixPathLength(path);
if (!Directory.Exists(path))
{
return;
}
if (includeReadonly)
{
foreach (var s in Directory.GetDirectories(path))
{
DeleteDirectory(s, true);
}
foreach (var f in Directory.GetFiles(path))
{
DeleteFile(f, true);
}
var dirAttr = File.GetAttributes(path);
if ((dirAttr & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
{
File.SetAttributes(path, dirAttr ^ FileAttributes.ReadOnly);
}
Directory.Delete(path, false);
}
else
{
DeleteDirectory(path);
}
}
public static bool CanWriteToFolder(string folder)
{
folder = FixPathLength(folder);
try
{
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
using (var stream = File.Create(Path.Combine(folder, Path.GetRandomFileName()), 1, FileOptions.DeleteOnClose))
{
}
return true;
}
catch
{
return false;
}
}
public static string ReadFileAsStringSafe(string path, int retryAttempts = 5)
{
path = FixPathLength(path);
IOException ioException = null;
for (int i = 0; i < retryAttempts; i++)
{
try
{
return File.ReadAllText(path);
}
catch (IOException exc)
{
logger.Debug($"Can't read from file, trying again. {path}");
ioException = exc;
Task.Delay(500).Wait();
}
}
throw new IOException($"Failed to read {path}", ioException);
}
public static byte[] ReadFileAsBytesSafe(string path, int retryAttempts = 5)
{
path = FixPathLength(path);
IOException ioException = null;
for (int i = 0; i < retryAttempts; i++)
{
try
{
return File.ReadAllBytes(path);
}
catch (IOException exc)
{
logger.Debug($"Can't read from file, trying again. {path}");
ioException = exc;
Task.Delay(500).Wait();
}
}
throw new IOException($"Failed to read {path}", ioException);
}
public static Stream CreateWriteFileStreamSafe(string path, int retryAttempts = 5)
{
path = FixPathLength(path);
IOException ioException = null;
for (int i = 0; i < retryAttempts; i++)
{
try
{
return new FileStream(path, FileMode.Create, FileAccess.ReadWrite);
}
catch (IOException exc)
{
logger.Debug($"Can't open write file stream, trying again. {path}");
ioException = exc;
Task.Delay(500).Wait();
}
}
throw new IOException($"Failed to read {path}", ioException);
}
public static Stream OpenReadFileStreamSafe(string path, int retryAttempts = 5)
{
path = FixPathLength(path);
IOException ioException = null;
for (int i = 0; i < retryAttempts; i++)
{
try
{
return new FileStream(path, FileMode.Open, FileAccess.Read);
}
catch (IOException exc)
{
logger.Debug($"Can't open read file stream, trying again. {path}");
ioException = exc;
Task.Delay(500).Wait();
}
}
throw new IOException($"Failed to read {path}", ioException);
}
public static void WriteStringLinesToFile(string path, IEnumerable<string> content, bool useUtf8 = false)
{
path = FixPathLength(path);
PrepareSaveFile(path);
if (useUtf8)
{
File.WriteAllLines(path, content, Encoding.UTF8);
}
else
{
File.WriteAllLines(path, content);
}
}
public static void WriteStringToFile(string path, string content, bool useUtf8 = true, bool createDirectory = true)
{
path = FixPathLength(path);
if (createDirectory)
{
string dir = Path.GetDirectoryName(path);
Directory.CreateDirectory(dir);
}
PrepareSaveFile(path);
if (useUtf8)
{
File.WriteAllText(path, content, new UTF8Encoding());
}
else
{
File.WriteAllText(path, content);
}
}
public static string[] ReadStringLinesFromFile(string path, bool useUtf8 = false)
{
path = FixPathLength(path);
if (useUtf8)
{
return File.ReadAllLines(path, Encoding.UTF8);
}
else
{
return File.ReadAllLines(path);
}
}
public static string ReadStringFromFile(string path, bool useUtf8 = false)
{
path = FixPathLength(path);
if (useUtf8)
{
return File.ReadAllText(path, Encoding.UTF8);
}
else
{
return File.ReadAllText(path);
}
}
public static void WriteStringToFileSafe(string path, string content, int retryAttempts = 5)
{
path = FixPathLength(path);
IOException ioException = null;
for (int i = 0; i < retryAttempts; i++)
{
try
{
PrepareSaveFile(path);
File.WriteAllText(path, content);
return;
}
catch (IOException exc)
{
logger.Error(exc, $"Can't write to a file, trying again. {path}");
ioException = exc;
Task.Delay(500).Wait();
}
}
//throw new IOException($"Failed to write to {path}", ioException);
}
public static void DeleteFileSafe(string path, int retryAttempts = 5)
{
path = FixPathLength(path);
if (!File.Exists(path))
{
return;
}
IOException ioException = null;
for (int i = 0; i < retryAttempts; i++)
{
try
{
File.Delete(path);
return;
}
catch (IOException exc)
{
logger.Debug($"Can't delete file, trying again. {path}");
ioException = exc;
Task.Delay(500).Wait();
}
catch (UnauthorizedAccessException exc)
{
logger.Error(exc, $"Can't delete file, UnauthorizedAccessException. {path}");
return;
}
}
throw new IOException($"Failed to delete {path}", ioException);
}
public static IEnumerable<DirectoryInfo> GetDirectories(string path, string regexPattern, RegexOptions regexOptions = RegexOptions.None, SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
path = FixPathLength(path);
Regex regex = new Regex(regexPattern, regexOptions);
try
{
return new DirectoryInfo(path).GetDirectories("*", searchOption)
.Where(x => regex.IsMatch(x.Name));
}
catch { return Enumerable.Empty<DirectoryInfo>(); }
}
public static IEnumerable<FileInfo> GetFiles(string path, string regexPattern, RegexOptions regexOptions = RegexOptions.None, SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
path = FixPathLength(path);
Regex regex = new Regex(regexPattern, regexOptions);
try
{
return new DirectoryInfo(path).GetFiles("*", searchOption)
.Where(x => regex.IsMatch(x.Name));
}
catch
{
return Enumerable.Empty<FileInfo>();
}
}
public static long GetFileSize(string path)
{
path = FixPathLength(path);
return new FileInfo(path).Length;
}
public static long GetFileSizeOnDisk(string path)
{
return GetFileSizeOnDisk(new FileInfo(FixPathLength(path)));
}
public static long GetFileSizeOnDisk(FileInfo info)
{
// From https://stackoverflow.com/a/3751135
uint dummy, sectorsPerCluster, bytesPerSector;
int result = GetDiskFreeSpaceW(info.Directory.Root.FullName, out sectorsPerCluster, out bytesPerSector, out dummy, out dummy);
if (result == 0) throw new System.ComponentModel.Win32Exception();
uint clusterSize = sectorsPerCluster * bytesPerSector;
uint hosize;
uint losize = GetCompressedFileSizeW(info.FullName, out hosize);
long size;
size = (long)hosize << 32 | losize;
return ((size + clusterSize - 1) / clusterSize) * clusterSize;
}
public static long GetDirectorySize(string path)
{
return GetDirectorySize(new DirectoryInfo(FixPathLength(path)));
}
private static long GetDirectorySize(DirectoryInfo dir)
{
try
{
long size = 0;
// Add file sizes.
FileInfo[] fis = dir.GetFiles();
foreach (FileInfo fi in fis)
{
size += fi.Length;
}
// Add subdirectory sizes.
DirectoryInfo[] dis = dir.GetDirectories();
foreach (DirectoryInfo di in dis)
{
size += GetDirectorySize(di);
}
return size;
}
catch
{
return 0;
}
}
public static long GetDirectorySizeOnDisk(string path)
{
return GetDirectorySizeOnDisk(new DirectoryInfo(FixPathLength(path)));
}
public static long GetDirectorySizeOnDisk(DirectoryInfo dirInfo)
{
long size = 0;
// Add file sizes.
foreach (FileInfo file in dirInfo.GetFiles())
{
size += GetFileSizeOnDisk(file);
}
// Add subdirectory sizes.
foreach (DirectoryInfo directory in dirInfo.GetDirectories())
{
size += GetDirectorySizeOnDisk(directory);
}
return size;
}
public static void CopyDirectory(string sourceDirName, string destDirName, bool copySubDirs = true, bool overwrite = true)
{
sourceDirName = FixPathLength(sourceDirName);
destDirName = FixPathLength(destDirName);
var dir = new DirectoryInfo(sourceDirName);
if (!dir.Exists)
{
throw new DirectoryNotFoundException(
"Source directory does not exist or could not be found: "
+ sourceDirName);
}
var dirs = dir.GetDirectories();
if (!Directory.Exists(destDirName))
{
Directory.CreateDirectory(destDirName);
}
var files = dir.GetFiles();
foreach (FileInfo file in files)
{
string temppath = Path.Combine(destDirName, file.Name);
file.CopyTo(temppath, overwrite);
}
if (copySubDirs)
{
foreach (DirectoryInfo subdir in dirs)
{
string temppath = Path.Combine(destDirName, subdir.Name);
CopyDirectory(subdir.FullName, temppath, copySubDirs);
}
}
}
public static bool DirectoryExists(string path)
{
return Directory.Exists(FixPathLength(path));
}
public static bool FileExists(string path)
{
return File.Exists(FixPathLength(path));
}
public static string FixPathLength(string path)
{
// Relative paths don't support long paths
// https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd
if (!Paths.IsFullPath(path))
{
return path;
}
if (path.Length >= 258 && !path.StartsWith(longPathPrefix))
{
if (path.StartsWith(@"\\"))
{
return longPathUncPrefix + path.Substring(2);
}
else
{
return longPathPrefix + path;
}
}
return path;
}
}
}