// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using EpicGames.Core; namespace UnrealBuildBase { /// /// Stores the state of a directory. May or may not exist. /// public class DirectoryItem { /// /// Full path to the directory on disk /// public readonly DirectoryReference Location; /// /// Cached value for whether the directory exists /// Lazy Info; /// /// Cached map of name to subdirectory item /// Dictionary? Directories; /// /// Cached map of name to file /// Dictionary? Files; /// /// Global map of location to item /// static ConcurrentDictionary LocationToItem = new ConcurrentDictionary(); /// /// Constructor /// /// Path to this directory /// Information about this directory private DirectoryItem(DirectoryReference Location, DirectoryInfo 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; }); } } /// /// The name of this directory /// public string Name => Info.Value.Name; /// /// The full name of this directory /// public string FullName => Location.FullName; /// /// Whether the directory exists or not /// public bool Exists => Info.Value.Exists; /// /// The last write time of the file. /// public DateTime LastWriteTimeUtc => Info.Value.LastWriteTimeUtc; /// /// Gets the parent directory item /// public DirectoryItem? GetParentDirectoryItem() { if (Info.Value.Parent == null) { return null; } else { return GetItemByDirectoryInfo(Info.Value.Parent); } } /// /// Gets a new directory item by combining the existing directory item with the given path fragments /// /// Base directory to append path fragments to /// The path fragments to append /// Directory item corresponding to the combined path public static DirectoryItem Combine(DirectoryItem BaseDirectory, params string[] Fragments) { return DirectoryItem.GetItemByDirectoryReference(DirectoryReference.Combine(BaseDirectory.Location, Fragments)); } /// /// Finds or creates a directory item from its location /// /// Path to the directory /// The directory item for this location public static DirectoryItem GetItemByPath(string Location) { return GetItemByDirectoryReference(new DirectoryReference(Location)); } /// /// Finds or creates a directory item from its location /// /// Path to the directory /// The directory item for this location public static DirectoryItem GetItemByDirectoryReference(DirectoryReference Location) { if (LocationToItem.TryGetValue(Location, out DirectoryItem? Result)) { return Result; } return LocationToItem.GetOrAdd(Location, new DirectoryItem(Location, new DirectoryInfo(Location.FullName))); } /// /// Finds or creates a directory item from a DirectoryInfo object /// /// Path to the directory /// The directory item for this location public static DirectoryItem GetItemByDirectoryInfo(DirectoryInfo Info) { DirectoryReference Location = new DirectoryReference(Info); if (LocationToItem.TryGetValue(Location, out DirectoryItem? Result)) { return Result; } return LocationToItem.GetOrAdd(Location, new DirectoryItem(Location, Info)); } /// /// Reset the contents of the directory and allow them to be fetched again /// public void ResetCachedInfo() { Info = new Lazy(() => { DirectoryInfo Info = Location.ToDirectoryInfo(); Info.Refresh(); return Info; }); Dictionary? PrevDirectories = Directories; if (PrevDirectories != null) { foreach (DirectoryItem SubDirectory in PrevDirectories.Values) { SubDirectory.ResetCachedInfo(); } Directories = null; } Dictionary? PrevFiles = Files; if (PrevFiles != null) { foreach (FileItem File in PrevFiles.Values) { File.ResetCachedInfo(); } Files = null; } } /// /// Resets all cached directory info. Significantly reduces performance; do not use unless strictly necessary. /// public static void ResetAllCachedInfo_SLOW() { foreach (DirectoryItem Item in LocationToItem.Values) { Item.Info = new Lazy(() => { DirectoryInfo Info = Item.Location.ToDirectoryInfo(); Info.Refresh(); return Info; }); Item.Directories = null; Item.Files = null; } FileItem.ResetAllCachedInfo_SLOW(); } /// /// Caches the subdirectories of this directories /// public void CacheDirectories() { if (Directories == null) { Dictionary NewDirectories; if (Info.Value.Exists) { DirectoryInfo[] Directories = Info.Value.GetDirectories(); NewDirectories = new Dictionary(Directories.Length, DirectoryReference.Comparer); foreach (DirectoryInfo SubDirectoryInfo in Directories) { NewDirectories.Add(SubDirectoryInfo.Name, DirectoryItem.GetItemByDirectoryInfo(SubDirectoryInfo)); } } else { NewDirectories = new Dictionary(DirectoryReference.Comparer); } Directories = NewDirectories; } } /// /// Enumerates all the subdirectories /// /// Sequence of subdirectory items public IEnumerable EnumerateDirectories() { CacheDirectories(); return Directories!.Values; } /// /// Attempts to get a sub-directory by name /// /// Name of the directory /// If successful receives the matching directory item with this name /// True if the file exists, false otherwise public bool TryGetDirectory(string Name, [NotNullWhen(true)] out DirectoryItem? OutDirectory) { if (Name.Length > 0 && Name[0] == '.') { if (Name.Length == 1) { OutDirectory = this; return true; } else if (Name.Length == 2 && Name[1] == '.') { OutDirectory = GetParentDirectoryItem(); return OutDirectory != null; } } CacheDirectories(); return Directories!.TryGetValue(Name, out OutDirectory); } /// /// Caches the files in this directory /// public void CacheFiles() { if (Files == null) { Dictionary NewFiles; if (Info.Value.Exists) { FileInfo[] FileInfos = Info.Value.GetFiles(); NewFiles = new Dictionary(FileInfos.Length, FileReference.Comparer); foreach (FileInfo FileInfo in FileInfos) { FileItem FileItem = FileItem.GetItemByFileInfo(FileInfo); FileItem.UpdateCachedDirectory(this); NewFiles[FileInfo.Name] = FileItem; } } else { NewFiles = new Dictionary(FileReference.Comparer); } Files = NewFiles; } } /// /// Enumerates all the files /// /// Sequence of FileItems public IEnumerable EnumerateFiles() { CacheFiles(); return Files!.Values; } /// /// Attempts to get a file from this directory by name. Unlike creating a file item and checking whether it exists, this will /// not create a permanent FileItem object if it does not exist. /// /// Name of the file /// If successful receives the matching file item with this name /// True if the file exists, false otherwise public bool TryGetFile(string Name, [NotNullWhen(true)] out FileItem? OutFile) { CacheFiles(); return Files!.TryGetValue(Name, out OutFile); } /// /// Formats this object as a string for debugging /// /// Location of the directory public override string ToString() { return Location.FullName; } /// /// Writes out all the enumerated files full names sorted to OutFile /// public static void WriteDebugFileWithAllEnumeratedFiles(string OutFile) { SortedSet AllFiles = new SortedSet(); foreach (DirectoryItem Item in DirectoryItem.LocationToItem.Values) { if (Item.Files != null) { foreach (FileItem File in Item.EnumerateFiles()) { AllFiles.Add(File.FullName); } } } File.WriteAllLines(OutFile, AllFiles); } } /// /// Helper functions for serialization /// public static class DirectoryItemExtensionMethods { /// /// Read a directory item from a binary archive /// /// Reader to serialize data from /// Instance of the serialized directory item public static DirectoryItem? ReadDirectoryItem(this BinaryArchiveReader Reader) { // Use lambda that doesn't require anything to be captured thus eliminating an allocation. return Reader.ReadObjectReference((BinaryArchiveReader Reader) => DirectoryItem.GetItemByDirectoryReference(Reader.ReadDirectoryReferenceNotNull())); } /// /// Write a directory item to a binary archive /// /// Writer to serialize data to /// Directory item to write public static void WriteDirectoryItem(this BinaryArchiveWriter Writer, DirectoryItem DirectoryItem) { // Use lambda that doesn't require anything to be captured thus eliminating an allocation. Writer.WriteObjectReference(DirectoryItem, (BinaryArchiveWriter Writer, DirectoryItem DirectoryItem) => Writer.WriteDirectoryReference(DirectoryItem.Location)); } } }