using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using Alphaleonis.Win32.Filesystem; using Wabbajack.Common.IO; using Directory = Alphaleonis.Win32.Filesystem.Directory; using DriveInfo = Alphaleonis.Win32.Filesystem.DriveInfo; using File = Alphaleonis.Win32.Filesystem.File; using FileInfo = Alphaleonis.Win32.Filesystem.FileInfo; using Path = Alphaleonis.Win32.Filesystem.Path; namespace Wabbajack.Common { public struct AbsolutePath : IPath, IComparable, IEquatable { #region ObjectEquality public bool Equals(AbsolutePath other) { return string.Equals(_path, other._path, StringComparison.InvariantCultureIgnoreCase); } public override bool Equals(object? obj) { return obj is AbsolutePath other && Equals(other); } #endregion public override int GetHashCode() { return _path.GetHashCode(StringComparison.InvariantCultureIgnoreCase); } public override string ToString() { return _path; } private readonly string _nullable_path; private string _path => _nullable_path ?? string.Empty; public AbsolutePath(string path, bool skipValidation = false) { if (path.Length == 3 && path[1] == ':' && (path[2] == '\\' || path[2] == '/')) { _nullable_path = path.Substring(0, 2) + '\\'; } else { _nullable_path = path.Replace("/", "\\").TrimEnd('\\'); } _nullable_path = _nullable_path.Replace("\\\\", "\\"); if (!skipValidation) { ValidateAbsolutePath(); } } public AbsolutePath(AbsolutePath path) { _nullable_path = path._path; } private void ValidateAbsolutePath() { if (Path.IsPathRooted(_path)) { return; } throw new InvalidDataException($"Absolute path must be absolute, got {_path}"); } public string Normalize() { return _path.Replace("/", "\\").TrimEnd('\\'); } public DriveInfo DriveInfo() { return new DriveInfo(Path.GetPathRoot(_path)); } public Extension Extension => Extension.FromPath(_path); public ValueTask OpenRead() { return OpenShared(); } public ValueTask Create() { var path = _path; return CircuitBreaker.WithAutoRetryAsync(async () => File.Open(path, FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 1024 * 32)); } public ValueTask OpenWrite() { var path = _path; return CircuitBreaker.WithAutoRetryAsync(async () => File.Open(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, 1024 * 32)); } public async Task WriteAllTextAsync(string text) { await using var fs = File.Create(_path); await fs.WriteAsync(Encoding.UTF8.GetBytes(text)); } public bool Exists => File.Exists(_path) || Directory.Exists(_path); public bool IsFile => File.Exists(_path); public bool IsDirectory => Directory.Exists(_path); public async Task DeleteDirectory(bool dontDeleteIfNotEmpty = false) { if (IsDirectory) { if (dontDeleteIfNotEmpty && (EnumerateFiles().Any() || EnumerateDirectories().Any())) return; await Utils.DeleteDirectory(this); } } public long Size => Exists ? new FileInfo(_path).Length : 0; public DateTime LastModified { get => File.GetLastWriteTime(_path); set => File.SetLastWriteTime(_path, value); } public DateTime LastModifiedUtc => File.GetLastWriteTimeUtc(_path); public AbsolutePath Parent => (AbsolutePath)Path.GetDirectoryName(_path); public RelativePath FileName => (RelativePath)Path.GetFileName(_path); public RelativePath FileNameWithoutExtension => (RelativePath)Path.GetFileNameWithoutExtension(_path); public bool IsEmptyDirectory => IsDirectory && !EnumerateFiles().Any(); public bool IsReadOnly { get { return new FileInfo(_path).IsReadOnly; } set { new FileInfo(_path).IsReadOnly = value; } } public void SetReadOnly(bool val) { IsReadOnly = true; } /// /// Returns the full path the folder that contains Wabbajack.Common. This will almost always be /// where all the binaries for the project reside. /// /// public static AbsolutePath EntryPoint { get { var location = Assembly.GetExecutingAssembly().Location ?? null; if (location == null) throw new ArgumentException("Could not find entry point."); return ((AbsolutePath)location).Parent; } } /// /// Returns the path to the Windows folder, most often c:\Windows /// public static AbsolutePath WindowsFolder { get { var path = KnownFolders.Windows.Path; if (path == null) throw new ArgumentNullException(nameof(path), "Unable to find path to the Windows folder!"); return new AbsolutePath(path); } } public AbsolutePath Root => (AbsolutePath)Path.GetPathRoot(_path); /// /// Moves this file to the specified location, will use Copy if required /// /// /// Replace the destination file if it exists public async ValueTask MoveToAsync(AbsolutePath otherPath, bool overwrite = false) { if (Root != otherPath.Root) { if (otherPath.Exists && overwrite) await otherPath.DeleteAsync(); await CopyToAsync(otherPath); await DeleteAsync(); return; } var path = _path; await CircuitBreaker.WithAutoRetryAsync(async () => File.Move(path, otherPath._path, overwrite ? MoveOptions.ReplaceExisting : MoveOptions.None)); } public RelativePath RelativeTo(AbsolutePath p) { var relPath = Path.GetRelativePath(p._path, _path); if (relPath == _path) throw new ArgumentException($"{_path} is not a subpath of {p._path}"); return new RelativePath(relPath); } public bool IsChildOf(AbsolutePath? parent) { if (parent is null) return false; var child = this; if (child == parent) return true; while (child.Parent.Exists) { if (child.Parent == parent) { return true; } child = child.Parent; } return false; } public async Task ReadAllTextAsync() { await using var fs = File.OpenRead(_path); return Encoding.UTF8.GetString(await fs.ReadAllAsync()); } /// /// Assuming the path is a folder, enumerate all the files in the folder /// /// if true, also returns files in sub-folders /// pattern to match against /// public IEnumerable EnumerateFiles(bool recursive = true, string pattern = "*") { if (!IsDirectory) return new AbsolutePath[0]; return Directory .EnumerateFiles(_path, pattern, recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) .Select(path => new AbsolutePath(path, true)); } #region Operators public static explicit operator string(AbsolutePath path) { return path._path; } public static explicit operator AbsolutePath(string path) { if (string.IsNullOrEmpty(path)) return default; return !Path.IsPathRooted(path) ? ((RelativePath)path).RelativeToEntryPoint() : new AbsolutePath(path); } public static bool operator ==(AbsolutePath a, AbsolutePath b) { return a.Equals(b); } public static bool operator !=(AbsolutePath a, AbsolutePath b) { return !a.Equals(b); } #endregion public void CreateDirectory() { Directory.CreateDirectory(_path); } public async Task DeleteAsync() { try { if (!IsFile) return; if (IsReadOnly) IsReadOnly = false; var path = _path; await CircuitBreaker.WithAutoRetryAsync(async () => File.Delete(path)); } catch (FileNotFoundException) { // ignore, it doesn't exist so why delete it? } } public void Delete() { if (!IsFile) return; if (IsReadOnly) IsReadOnly = false; var path = _path; CircuitBreaker.WithAutoRetry(async () => File.Delete(path)); } public bool InFolder(AbsolutePath folder) { return _path.StartsWith(folder._path + Path.DirectorySeparator, StringComparison.OrdinalIgnoreCase); } public async Task ReadAllBytesAsync() { await using var f = await OpenShared(); return await f.ReadAllAsync(); } public AbsolutePath WithExtension(Extension hashFileExtension) { return new AbsolutePath(_path + (string)hashFileExtension, true); } public AbsolutePath ReplaceExtension(Extension extension) { return new AbsolutePath( Path.Combine(Path.GetDirectoryName(_path), Path.GetFileNameWithoutExtension(_path) + (string)extension), true); } public AbsolutePath AppendToName(string toAppend) { return new AbsolutePath( Path.Combine(Path.GetDirectoryName(_path), Path.GetFileNameWithoutExtension(_path) + toAppend + (string)Extension)); } public AbsolutePath Combine(params RelativePath[] paths) { return new AbsolutePath(Path.Combine(paths.Select(s => (string)s).Cons(_path).ToArray())); } public AbsolutePath Combine(params string[] paths) { return new AbsolutePath(Path.Combine(paths.Cons(_path).ToArray())); } public IEnumerable ReadAllLines() { return File.ReadAllLines(_path); } public async Task WriteAllBytesAsync(byte[] data) { await using var fs = await Create(); await fs.WriteAsync(data); } public async Task WriteAllAsync(Stream data, bool disposeDataAfter = true) { await using var fs = await Create(); await data.CopyToAsync(fs); await data.FlushAsync(); if (disposeDataAfter) await data.DisposeAsync(); } [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)] private static extern bool CreateHardLink(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes); public bool HardLinkTo(AbsolutePath destination) { Utils.Log($"Hard Linking {_path} to {destination}"); return CreateHardLink((string)destination, (string)this, IntPtr.Zero); } public async ValueTask HardLinkIfOversize(AbsolutePath destination) { if (!destination.Parent.Exists) destination.Parent.CreateDirectory(); if (Root == destination.Root && Consts.SupportedBSAs.Contains(Extension)) { if (HardLinkTo(destination)) return; } await CopyToAsync(destination); } public async Task> ReadAllLinesAsync() { return (await ReadAllTextAsync()).Split(new[] {'\n', '\r'}, StringSplitOptions.RemoveEmptyEntries); } public static AbsolutePath GetCurrentDirectory() { return new AbsolutePath(Directory.GetCurrentDirectory()); } public async Task CopyToAsync(AbsolutePath destFile) { await using var src = await OpenRead(); await using var dest = await destFile.Create(); await src.CopyToAsync(dest); } public IEnumerable EnumerateDirectories(bool recursive = true) { return Directory.EnumerateDirectories(_path, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) .Select(p => (AbsolutePath)p); } public async Task WriteAllLinesAsync(params string[] strings) { await WriteAllTextAsync(string.Join("\r\n",strings)); } public int CompareTo(AbsolutePath other) { return string.Compare(_path, other._path, StringComparison.OrdinalIgnoreCase); } public string ReadAllText() { return File.ReadAllText(_path); } public ValueTask OpenShared() { var path = _path; return CircuitBreaker.WithAutoRetryAsync(async () => File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, bufferSize: 4096, useAsync: true)); } public ValueTask WriteShared() { var path = _path; return CircuitBreaker.WithAutoRetryAsync(async () => File.Open(path, FileMode.Open, FileAccess.Write, FileShare.ReadWrite, bufferSize: 4096, useAsync: true)); } public async Task CopyDirectoryToAsync(AbsolutePath destination) { destination.CreateDirectory(); foreach (var file in EnumerateFiles()) { var dest = file.RelativeTo(this).RelativeTo(destination); await file.CopyToAsync(dest); } } } }