// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; using System.Text; using Tools.DotNETCommon; using Tools.DotNETCommon.Perforce; namespace BuildAgent.Workspace.Common { /// /// Represents a file within a stream /// class StreamFileInfo { /// /// Name of this file /// public readonly string Name; /// /// Length of the file, as reported by the server (actual size on disk may be different due to workspace options). /// public readonly long Length; /// /// Content id for this file /// public readonly FileContentId ContentId; /// /// The parent directory /// public readonly StreamDirectoryInfo Directory; /// /// The depot file and revision that need to be synced for this file /// public readonly string DepotFileAndRevision; /// /// Constructor /// /// Name of this file /// Length of the file on the server /// Content id for this file /// The parent directory /// The depot file and revision that need to be synced for this file public StreamFileInfo(string Name, long Length, FileContentId ContentId, StreamDirectoryInfo Directory, string DepotFileAndRevision) { this.Name = Name; this.Length = Length; this.ContentId = ContentId; this.Directory = Directory; this.DepotFileAndRevision = DepotFileAndRevision; } /// /// Constructor for reading a file info from disk /// /// Binary reader to read data from /// Parent directory public StreamFileInfo(BinaryReader Reader, StreamDirectoryInfo Directory) { this.Name = Reader.ReadString(); this.Length = Reader.ReadInt64(); this.ContentId = new FileContentId(Reader); this.Directory = Directory; this.DepotFileAndRevision = Reader.ReadString(); } /// /// Save the file info to disk /// /// Writer to output to public void Write(BinaryWriter Writer) { Writer.Write(Name); Writer.Write(Length); ContentId.Write(Writer); Writer.Write(DepotFileAndRevision); } /// /// Get the path to this file relative to the root of the stream /// /// Relative path to the file public string GetRelativePath() { StringBuilder Builder = new StringBuilder(); Directory.AppendPath(Builder); Builder.Append(Name); return Builder.ToString(); } /// /// Format the path to the file for the debugger /// /// Path to the file public override string ToString() { return GetRelativePath(); } } /// /// Information about a directory within a stream /// class StreamDirectoryInfo { /// /// The current signature for saved directory objects /// static readonly byte[] CurrentSignature = { (byte)'W', (byte)'S', (byte)'D', 1 }; /// /// The directory name /// public readonly string Name; /// /// The parent directory /// public readonly StreamDirectoryInfo ParentDirectory; /// /// Map of name to file within the directory /// public Dictionary NameToFile = new Dictionary(StringComparer.Ordinal); /// /// Map of name to subdirectory /// public Dictionary NameToSubDirectory = new Dictionary(FileUtils.PlatformPathComparer); /// /// Constructor /// public StreamDirectoryInfo() : this("", null) { } /// /// Constructor /// /// Name of the directory /// Parent directory public StreamDirectoryInfo(string Name, StreamDirectoryInfo ParentDirectory) { this.Name = Name; this.ParentDirectory = ParentDirectory; } /// /// Constructor for reading from disk /// /// Reader to read data from /// The parent directory to initialize this directory with public StreamDirectoryInfo(BinaryReader Reader, StreamDirectoryInfo ParentDirectory) { this.Name = Reader.ReadString(); this.ParentDirectory = ParentDirectory; int NumFiles = Reader.ReadInt32(); for(int Idx = 0; Idx < NumFiles; Idx++) { StreamFileInfo File = new StreamFileInfo(Reader, this); NameToFile.Add(File.Name, File); } int NumDirectories = Reader.ReadInt32(); for(int Idx = 0; Idx < NumDirectories; Idx++) { StreamDirectoryInfo SubDirectory = new StreamDirectoryInfo(Reader, this); NameToSubDirectory.Add(SubDirectory.Name, SubDirectory); } } /// /// Writes the contents of this stream to disk /// /// Writer to serialize to public void Write(BinaryWriter Writer) { Writer.Write(Name); Writer.Write(NameToFile.Count); foreach(StreamFileInfo File in NameToFile.Values) { File.Write(Writer); } Writer.Write(NameToSubDirectory.Count); foreach(StreamDirectoryInfo SubDirectory in NameToSubDirectory.Values) { SubDirectory.Write(Writer); } } /// /// Load a stream directory from a file on disk /// /// File to read from /// New StreamDirectoryInfo object public static StreamDirectoryInfo Load(FileReference InputFile) { using(FileStream Stream = File.Open(InputFile.FullName, FileMode.Open, FileAccess.Read, FileShare.Read)) { byte[] Signature = new byte[CurrentSignature.Length]; if(Stream.Read(Signature, 0, CurrentSignature.Length) != CurrentSignature.Length) { throw new InvalidDataException(String.Format("Unable to read signature bytes from {0}", InputFile)); } if(!Enumerable.SequenceEqual(Signature, CurrentSignature)) { throw new InvalidDataException(String.Format("Cached stream contents at {0} has incorrect signature", InputFile)); } using(GZipStream CompressedStream = new GZipStream(Stream, CompressionMode.Decompress)) { using(BinaryReader Reader = new BinaryReader(CompressedStream, Encoding.UTF8, true)) { return new StreamDirectoryInfo(Reader, null); } } } } /// /// Saves the contents of this object to disk /// /// The output file to write to public void Save(FileReference OutputFile) { using(FileStream Stream = File.Open(OutputFile.FullName, FileMode.CreateNew, FileAccess.Write, FileShare.Read)) { Stream.Write(CurrentSignature, 0, CurrentSignature.Length); using(GZipStream CompressedStream = new GZipStream(Stream, CompressionMode.Compress)) { using(BinaryWriter Writer = new BinaryWriter(CompressedStream, Encoding.UTF8, true)) { Write(Writer); } } } } /// /// Get all the files in this directory /// /// List of files public List GetFiles() { List Files = new List(); AppendFiles(Files); return Files; } /// /// Append the contents of this directory and subdirectories to a list /// /// List to append to public void AppendFiles(List Files) { foreach(StreamDirectoryInfo SubDirectory in NameToSubDirectory.Values) { SubDirectory.AppendFiles(Files); } Files.AddRange(NameToFile.Values); } /// /// Gets the relative path to this directory, with a trailing slash /// /// Relative path to this directory public string GetRelativePath() { StringBuilder Builder = new StringBuilder(); AppendPath(Builder); return Builder.ToString(); } /// /// Append the relative path to this directory to the given string builder /// /// String builder to append to public void AppendPath(StringBuilder Builder) { if(ParentDirectory != null) { ParentDirectory.AppendPath(Builder); Builder.Append(Name); } Builder.Append('/'); } /// /// Format the path to the directory for debugging /// /// Path to the directory public override string ToString() { return GetRelativePath(); } } }