File: TempFileUtilities.cs
Web Access
Project: ..\..\..\src\Utilities\Microsoft.Build.Utilities.csproj (Microsoft.Build.Utilities.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.IO;
using System.Runtime.CompilerServices;
using Microsoft.Build.Shared.FileSystem;
 
#nullable disable
 
namespace Microsoft.Build.Shared
{
    /// <summary>
    /// This class contains utility methods for file IO.
    /// It is in a separate file so that it can be selectively included into an assembly.
    /// </summary>
    internal static partial class FileUtilities
    {
        // For the current user, these correspond to read, write, and execute permissions.
        // Lower order bits correspond to the same for "group" or "other" users.
        private const int userRWX = 0x100 | 0x80 | 0x40;
        private static string tempFileDirectory = null;
        private const string msbuildTempFolderPrefix = "MSBuildTemp";
 
        internal static string TempFileDirectory
        {
            get
            {
                return tempFileDirectory ??= CreateFolderUnderTemp();
            }
        }
 
        internal static void ClearTempFileDirectory()
        {
            tempFileDirectory = null;
        }
 
        // For all native calls, directly check their return values to prevent bad actors from getting in between checking if a directory exists and returning it.
        private static string CreateFolderUnderTemp()
        {
            // On windows Username with Unicode chars can give issues, so we dont append username to the temp folder name.
            string msbuildTempFolder = NativeMethodsShared.IsWindows ?
                msbuildTempFolderPrefix :
                msbuildTempFolderPrefix + Environment.UserName;
 
            string basePath = Path.Combine(Path.GetTempPath(), msbuildTempFolder);
 
            if (NativeMethodsShared.IsLinux && NativeMethodsShared.mkdir(basePath, userRWX) != 0)
            {
                if (NativeMethodsShared.chmod(basePath, userRWX) == 0)
                {
                    // Current user owns this file; we can read and write to it. It is reasonable here to assume it was created properly by MSBuild and can be used
                    // for temporary files.
                }
                else
                {
                    // Another user created a folder pretending to be us! Find a folder we can actually use.
                    int extraBits = 0;
                    string pathToCheck = basePath + extraBits;
                    while (NativeMethodsShared.mkdir(pathToCheck, userRWX) != 0 && NativeMethodsShared.chmod(pathToCheck, userRWX) != 0)
                    {
                        extraBits++;
                        pathToCheck = basePath + extraBits;
                    }
 
                    basePath = pathToCheck;
                }
            }
            else
            {
                Directory.CreateDirectory(basePath);
            }
 
            return FileUtilities.EnsureTrailingSlash(basePath);
        }
 
        /// <summary>
        /// Generates a unique directory name in the temporary folder.
        /// Caller must delete when finished.
        /// </summary>
        /// <param name="createDirectory"></param>
        /// <param name="subfolder"></param>
        internal static string GetTemporaryDirectory(bool createDirectory = true, string subfolder = null)
        {
            string temporaryDirectory = Path.Combine(TempFileDirectory, "Temporary" + Guid.NewGuid().ToString("N"), subfolder ?? string.Empty);
 
            if (createDirectory)
            {
                Directory.CreateDirectory(temporaryDirectory);
            }
 
            return temporaryDirectory;
        }
 
        /// <summary>
        /// Generates a unique temporary file name with a given extension in the temporary folder.
        /// File is guaranteed to be unique.
        /// Extension may have an initial period.
        /// File will NOT be created.
        /// May throw IOException.
        /// </summary>
        internal static string GetTemporaryFileName()
        {
            return GetTemporaryFileName(".tmp");
        }
 
        /// <summary>
        /// Generates a unique temporary file name with a given extension in the temporary folder.
        /// File is guaranteed to be unique.
        /// Extension may have an initial period.
        /// File will NOT be created.
        /// May throw IOException.
        /// </summary>
        internal static string GetTemporaryFileName(string extension)
        {
            return GetTemporaryFile(null, null, extension, false);
        }
 
        /// <summary>
        /// Generates a unique temporary file name with a given extension in the temporary folder.
        /// If no extension is provided, uses ".tmp".
        /// File is guaranteed to be unique.
        /// Caller must delete it when finished.
        /// </summary>
        internal static string GetTemporaryFile()
        {
            return GetTemporaryFile(".tmp");
        }
 
        /// <summary>
        /// Generates a unique temporary file name with a given extension in the temporary folder.
        /// File is guaranteed to be unique.
        /// Caller must delete it when finished.
        /// </summary>
        internal static string GetTemporaryFile(string fileName, string extension, bool createFile)
        {
            return GetTemporaryFile(null, fileName, extension, createFile);
        }
 
        /// <summary>
        /// Generates a unique temporary file name with a given extension in the temporary folder.
        /// File is guaranteed to be unique.
        /// Extension may have an initial period.
        /// Caller must delete it when finished.
        /// May throw IOException.
        /// </summary>
        internal static string GetTemporaryFile(string extension)
        {
            return GetTemporaryFile(null, null, extension);
        }
 
        /// <summary>
        /// Creates a file with unique temporary file name with a given extension in the specified folder.
        /// File is guaranteed to be unique.
        /// Extension may have an initial period.
        /// If folder is null, the temporary folder will be used.
        /// Caller must delete it when finished.
        /// May throw IOException.
        /// </summary>
        internal static string GetTemporaryFile(string directory, string fileName, string extension, bool createFile = true)
        {
            ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(directory, nameof(directory));
 
            try
            {
                directory ??= TempFileDirectory;
 
                // If the extension needs a dot prepended, do so.
                if (extension is null)
                {
                    extension = string.Empty;
                }
                else if (extension.Length > 0 && extension[0] != '.')
                {
                    extension = '.' + extension;
                }
 
                // If the fileName is null, use tmp{Guid}; otherwise use fileName.
                if (string.IsNullOrEmpty(fileName))
                {
                    fileName = $"tmp{Guid.NewGuid():N}";
                }
 
                Directory.CreateDirectory(directory);
 
                string file = Path.Combine(directory, $"{fileName}{extension}");
 
                ErrorUtilities.VerifyThrow(!FileSystems.Default.FileExists(file), "Guid should be unique");
 
                if (createFile)
                {
                    File.WriteAllText(file, string.Empty);
                }
 
                return file;
            }
            catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
            {
                throw new IOException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("Shared.FailedCreatingTempFile", ex.Message), ex);
            }
        }
 
        internal static void CopyDirectory(string source, string dest)
        {
            Directory.CreateDirectory(dest);
 
            DirectoryInfo sourceInfo = new DirectoryInfo(source);
            foreach (var fileInfo in sourceInfo.GetFiles())
            {
                string destFile = Path.Combine(dest, fileInfo.Name);
                fileInfo.CopyTo(destFile);
            }
            foreach (var subdirInfo in sourceInfo.GetDirectories())
            {
                string destDir = Path.Combine(dest, subdirInfo.Name);
                CopyDirectory(subdirInfo.FullName, destDir);
            }
        }
 
        public sealed class TempWorkingDirectory : IDisposable
        {
            public string Path { get; }
 
            public TempWorkingDirectory(string sourcePath,
#if !CLR2COMPATIBILITY
                [CallerMemberName]
#endif
            string name = null)
            {
                Path = name == null
                    ? GetTemporaryDirectory()
                    : System.IO.Path.Combine(TempFileDirectory, name);
 
                if (FileSystems.Default.DirectoryExists(Path))
                {
                    Directory.Delete(Path, true);
                }
 
                CopyDirectory(sourcePath, Path);
            }
 
            public void Dispose()
            {
                Directory.Delete(Path, true);
            }
        }
    }
}