// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Concurrent; using System.Diagnostics; using System.IO; using System.Threading; using EpicGames.Core; using Microsoft.Extensions.Logging; namespace UnrealBuildBase { /// /// Represents a file on disk that is used as an input or output of a build action. FileItem instances are unique for a given path. Use FileItem.GetItemByFileReference /// to get the FileItem for a specific path. /// public class FileItem : IComparable, IEquatable { /// /// The directory containing this file /// DirectoryItem? CachedDirectory; /// /// Location of this file /// public readonly FileReference Location; /// /// The information about the file. /// Lazy Info; /// /// A case-insensitive dictionary that's used to map each unique file name to a single FileItem object. /// static ConcurrentDictionary UniqueSourceFileMap = new ConcurrentDictionary(); /// /// Constructor /// /// Location of the file /// File info private FileItem(FileReference Location, FileInfo Info) { this.Location = Location; if (RuntimePlatform.IsWindows) { this.Info = new Lazy(Info); } else { // For some reason we need to call an extra Refresh on linux/mac to not get wrong results from "Exists" this.Info = new Lazy(() => { Info.Refresh(); return Info; }); } } /// /// Name of this file /// public string Name => Info.Value.Name; /// /// Full name of this file /// public string FullName => Location.FullName; /// /// Accessor for the absolute path to the file /// public string AbsolutePath => Location.FullName; /// /// Gets the directory that this file is in /// public DirectoryItem Directory { get { if (CachedDirectory == null) { CachedDirectory = DirectoryItem.GetItemByDirectoryReference(Location.Directory); } return CachedDirectory; } } /// /// Whether the file exists. /// public bool Exists => Info.Value.Exists; /// /// Size of the file if it exists, otherwise -1 /// public long Length => Info.Value.Length; /// /// The attributes for this file /// public FileAttributes Attributes => Info.Value.Attributes; /// /// The last write time of the file. /// public DateTime LastWriteTimeUtc => Info.Value.LastWriteTimeUtc; /// /// The creation time of the file. /// public DateTime CreationTimeUtc => Info.Value.CreationTimeUtc; /// /// Determines if the file has the given extension /// /// The extension to check for /// True if the file has the given extension, false otherwise public bool HasExtension(string Extension) { return Location.HasExtension(Extension); } /// /// Gets the directory containing this file /// /// DirectoryItem for the directory containing this file public DirectoryItem GetDirectoryItem() { return Directory; } /// /// Updates the cached directory for this file. Used by DirectoryItem when enumerating files, to avoid having to look this up later. /// /// The directory that this file is in public void UpdateCachedDirectory(DirectoryItem Directory) { Debug.Assert(Directory.Location == Location.Directory); CachedDirectory = Directory; } /// /// Gets a FileItem corresponding to the given path /// /// Path for the FileItem /// The FileItem that represents the given file path. public static FileItem GetItemByPath(string FilePath) { return GetItemByFileReference(new FileReference(FilePath)); } /// /// Gets a FileItem for a given path /// /// Information about the file /// The FileItem that represents the given a full file path. public static FileItem GetItemByFileInfo(FileInfo Info) { FileReference Location = new FileReference(Info); if (UniqueSourceFileMap.TryGetValue(Location, out FileItem? Result)) // 99.9% reads, so faster to front with a TryGet before GetOrAdd { return Result; } return UniqueSourceFileMap.GetOrAdd(Location, new FileItem(Location, Info)); } /// /// Gets a FileItem for a given path /// /// Location of the file /// The FileItem that represents the given a full file path. public static FileItem GetItemByFileReference(FileReference Location) { if (UniqueSourceFileMap.TryGetValue(Location, out FileItem? Result)) // 99.9% reads, so faster to front with a TryGet before GetOrAdd { return Result; } return UniqueSourceFileMap.GetOrAdd(Location, new FileItem(Location, Location.ToFileInfo())); } /// /// Deletes the file. /// public void Delete(ILogger Logger) { Debug.Assert(Exists); int MaxRetryCount = 3; int DeleteTryCount = 0; bool bFileDeletedSuccessfully = false; do { // If this isn't the first time through, sleep a little before trying again if (DeleteTryCount > 0) { Thread.Sleep(1000); } DeleteTryCount++; try { // Delete the destination file if it exists FileInfo DeletedFileInfo = new FileInfo(AbsolutePath); if (DeletedFileInfo.Exists) { DeletedFileInfo.IsReadOnly = false; DeletedFileInfo.Delete(); } // Success! bFileDeletedSuccessfully = true; } catch (Exception Ex) { Logger.LogInformation(Ex, "Failed to delete file '{Location}'", Location); Logger.LogInformation(" Exception: {Message}", Ex.Message); if (DeleteTryCount < MaxRetryCount) { Logger.LogInformation("Attempting to retry..."); } else { Logger.LogError("ERROR: Exhausted all retries!"); } } } while (!bFileDeletedSuccessfully && (DeleteTryCount < MaxRetryCount)); } /// /// Resets the cached file info /// public void ResetCachedInfo() { Info = new Lazy(() => { FileInfo Info = Location.ToFileInfo(); Info.Refresh(); return Info; }); } /// /// Resets the cached info, if the FileInfo is not found don't create a new entry /// public static void ResetCachedInfo(string Path) { if (UniqueSourceFileMap.TryGetValue(new FileReference(Path), out FileItem? Result)) { Result.ResetCachedInfo(); } } /// /// Resets all cached file info. Significantly reduces performance; do not use unless strictly necessary. /// public static void ResetAllCachedInfo_SLOW() { foreach (FileItem Item in UniqueSourceFileMap.Values) { Item.ResetCachedInfo(); } } /// /// Return the path to this FileItem to debugging /// /// Absolute path to this file item public override string ToString() { return AbsolutePath; } #region IComparable, IEquatbale public int CompareTo(FileItem? other) { return FullName.CompareTo(other?.FullName); } public bool Equals(FileItem? other) { return FullName.Equals(other?.FullName); } public override bool Equals(object? obj) { if (ReferenceEquals(this, obj)) { return true; } if (obj is null) { return false; } return Equals(obj as FileItem); } public override int GetHashCode() { return FullName.GetHashCode(); } public static bool operator ==(FileItem? left, FileItem? right) { if (left is null) { return right is null; } return left.Equals(right); } public static bool operator !=(FileItem? left, FileItem? right) { return !(left == right); } public static bool operator <(FileItem? left, FileItem? right) { return left is null ? right is not null : left.CompareTo(right) < 0; } public static bool operator <=(FileItem? left, FileItem? right) { return left is null || left.CompareTo(right) <= 0; } public static bool operator >(FileItem? left, FileItem? right) { return left is not null && left.CompareTo(right) > 0; } public static bool operator >=(FileItem? left, FileItem? right) { return left is null ? right is null : left.CompareTo(right) >= 0; } #endregion } /// /// Helper functions for serialization /// public static class FileItemExtensionMethods { /// /// Read a file item from a binary archive /// /// Reader to serialize data from /// Instance of the serialized file item public static FileItem? ReadFileItem(this BinaryArchiveReader Reader) { return Reader.ReadObjectReference((BinaryArchiveReader Reader) => FileItem.GetItemByFileReference(Reader.ReadFileReference())); } /// /// Write a file item to a binary archive /// /// Writer to serialize data to /// File item to write public static void WriteFileItem(this BinaryArchiveWriter Writer, FileItem? FileItem) { Writer.WriteObjectReference(FileItem!, () => Writer.WriteFileReference(FileItem!.Location)); } /// /// Read a file item as a DirectoryItem and name. This is slower than reading it directly, but results in a significantly smaller archive /// where most files are in the same directories. /// /// Archive to read from /// FileItem read from the archive static FileItem ReadCompactFileItemData(this BinaryArchiveReader Reader) { DirectoryItem Directory = Reader.ReadDirectoryItem()!; string Name = Reader.ReadString()!; FileItem FileItem = FileItem.GetItemByFileReference(FileReference.Combine(Directory.Location, Name)); FileItem.UpdateCachedDirectory(Directory); return FileItem; } /// /// Read a file item in a format which de-duplicates directory names. /// /// Reader to serialize data from /// Instance of the serialized file item public static FileItem ReadCompactFileItem(this BinaryArchiveReader Reader) { // Use lambda that doesn't require anything to be captured thus eliminating an allocation. return Reader.ReadObjectReference((BinaryArchiveReader Reader) => ReadCompactFileItemData(Reader))!; } /// /// Writes a file item in a format which de-duplicates directory names. /// /// Writer to serialize data to /// File item to write public static void WriteCompactFileItem(this BinaryArchiveWriter Writer, FileItem FileItem) { // Use lambda that doesn't require anything to be captured thus eliminating an allocation. Writer.WriteObjectReference(FileItem, (BinaryArchiveWriter Writer, FileItem FileItem) => { Writer.WriteDirectoryItem(FileItem.GetDirectoryItem()); Writer.WriteString(FileItem.Name); }); } } }