// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
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
///
Lazy 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(-1, 100000);// [];
///
/// Constructor
///
/// Location of the file
/// File info
private FileItem(FileReference Location, FileInfo Info)
{
this.Location = Location;
CachedDirectory = new(() => DirectoryItem.GetItemByDirectoryReference(Location.Directory));
// For some reason we need to call an extra Refresh on Linux/Mac to not get wrong results from "Exists"
this.Info = OperatingSystem.IsWindows()
? new(Info)
: new(() =>
{
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 => CachedDirectory.Value;
///
/// 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) => Location.HasExtension(Extension);
///
/// Gets the directory containing this file
///
/// DirectoryItem for the directory containing this file
public DirectoryItem GetDirectoryItem() => 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 = new(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) => GetItemByFileReference(new(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) => GetItemByFileReference(new(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)
=> UniqueSourceFileMap.GetOrAdd(Location, static loc => new FileItem(loc, loc.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(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(() =>
{
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(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()
{
UniqueSourceFileMap.Values.AsParallel().ForAll(Item => Item.ResetCachedInfo());
}
///
/// Return the path to this FileItem to debugging
///
/// Absolute path to this file item
public override string ToString() => Location.ToString();
#region IComparable, IEquatbale
///
public int CompareTo(FileItem? other) => Location.CompareTo(other?.Location);
///
public bool Equals(FileItem? other) => Location.Equals(other?.Location);
///
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() => Location.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(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
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static FileItem ReadCompactFileItem(this BinaryArchiveReader Reader)
{
// Use lambda that doesn't require anything to be captured thus eliminating an allocation.
return Reader.ReadObjectReference(ReadCompactFileItemData)!;
}
///
/// 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, (Writer, FileItem) =>
{
Writer.WriteDirectoryItem(FileItem.GetDirectoryItem());
Writer.WriteString(FileItem.Name);
});
}
}
}