// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Diagnostics; using System.Threading; using System.Runtime.Serialization; using EpicGames.Core; using System.Collections.Concurrent; 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 { /// /// The directory containing this file /// DirectoryItem? CachedDirectory; /// /// Location of this file /// public readonly FileReference Location; /// /// The information about the file. /// FileInfo 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; this.Info = Info; } /// /// Name of this file /// public string Name { get { return Info.Name; } } /// /// Full name of this file /// public string FullName { get { return Location.FullName; } } /// /// Accessor for the absolute path to the file /// public string AbsolutePath { get { return 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 { get { return Info.Exists; } } /// /// Size of the file if it exists, otherwise -1 /// public long Length { get { return Info.Length; } } /// /// The attributes for this file /// public FileAttributes Attributes { get { return Info.Attributes; } } /// /// The last write time of the file. /// public DateTime LastWriteTimeUtc { get { return Info.LastWriteTimeUtc; } } /// /// 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); FileItem? Result; if (!UniqueSourceFileMap.TryGetValue(Location, out Result)) { FileItem NewFileItem = new FileItem(Location, Info); if(UniqueSourceFileMap.TryAdd(Location, NewFileItem)) { Result = NewFileItem; } else { Result = UniqueSourceFileMap[Location]; } } return Result; } /// /// 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) { FileItem? Result; if (!UniqueSourceFileMap.TryGetValue(Location, out Result)) { FileItem NewFileItem = new FileItem(Location, Location.ToFileInfo()); if(UniqueSourceFileMap.TryAdd(Location, NewFileItem)) { Result = NewFileItem; } else { Result = UniqueSourceFileMap[Location]; } } return Result; } /// /// Deletes the file. /// public void Delete() { 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) { Log.TraceInformation("Failed to delete file '" + AbsolutePath + "'"); Log.TraceInformation(" Exception: " + Ex.Message); if (DeleteTryCount < MaxRetryCount) { Log.TraceInformation("Attempting to retry..."); } else { Log.TraceInformation("ERROR: Exhausted all retries!"); } } } while (!bFileDeletedSuccessfully && (DeleteTryCount < MaxRetryCount)); } /// /// Resets the cached file info /// public void ResetCachedInfo() { Info = Location.ToFileInfo(); } /// /// 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; } } /// /// 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(() => 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) { return Reader.ReadObjectReference(() => 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) { Writer.WriteObjectReference(FileItem, () => { Writer.WriteDirectoryItem(FileItem.GetDirectoryItem()); Writer.WriteString(FileItem.Name); }); } } }