File: ShadowCopyManager.cs
Web Access
Project: ..\..\..\src\RazorSdk\Tool\Microsoft.NET.Sdk.Razor.Tool.csproj (rzc)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using System.Globalization;
 
namespace Microsoft.NET.Sdk.Razor.Tool
{
    // Note that this class has no thread-safety guarantees. The caller should use a lock 
    // if concurrency is required.
    internal class ShadowCopyManager : IDisposable
    {
        // Note that this class uses the *existance* of the Mutex to lock a directory.
        //
        // Nothing in this code actually ever acquires the Mutex, we just try to see if it exists
        // already.
        private readonly Mutex _mutex;
 
        private int _counter;
 
        public ShadowCopyManager(string baseDirectory = null)
        {
            BaseDirectory = baseDirectory ?? Path.Combine(Path.GetTempPath(), "Razor", "ShadowCopy");
 
            var guid = Guid.NewGuid().ToString("N").ToLowerInvariant();
            UniqueDirectory = Path.Combine(BaseDirectory, guid);
 
            _mutex = new Mutex(initiallyOwned: false, name: guid);
 
            Directory.CreateDirectory(UniqueDirectory);
        }
 
        public string BaseDirectory { get; }
 
        public string UniqueDirectory { get; }
 
        public string AddAssembly(string filePath)
        {
            var assemblyDirectory = CreateUniqueDirectory();
 
            var destination = Path.Combine(assemblyDirectory, Path.GetFileName(filePath));
            CopyFile(filePath, destination);
 
            var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(filePath);
            var resourcesNameWithoutExtension = fileNameWithoutExtension + ".resources";
            var resourcesNameWithExtension = resourcesNameWithoutExtension + ".dll";
 
            foreach (var directory in Directory.EnumerateDirectories(Path.GetDirectoryName(filePath)))
            {
                var directoryName = Path.GetFileName(directory);
 
                var resourcesPath = Path.Combine(directory, resourcesNameWithExtension);
                if (File.Exists(resourcesPath))
                {
                    var resourcesShadowCopyPath = Path.Combine(assemblyDirectory, directoryName, resourcesNameWithExtension);
                    CopyFile(resourcesPath, resourcesShadowCopyPath);
                }
 
                resourcesPath = Path.Combine(directory, resourcesNameWithoutExtension, resourcesNameWithExtension);
                if (File.Exists(resourcesPath))
                {
                    var resourcesShadowCopyPath = Path.Combine(assemblyDirectory, directoryName, resourcesNameWithoutExtension, resourcesNameWithExtension);
                    CopyFile(resourcesPath, resourcesShadowCopyPath);
                }
            }
 
            return destination;
        }
 
        public void Dispose()
        {
            _mutex.ReleaseMutex();
        }
 
        public Task PurgeUnusedDirectoriesAsync()
        {
            return Task.Run((Action)PurgeUnusedDirectories);
        }
 
        private string CreateUniqueDirectory()
        {
            var id = _counter++;
 
            var directory = Path.Combine(UniqueDirectory, id.ToString(CultureInfo.InvariantCulture));
            Directory.CreateDirectory(directory);
            return directory;
        }
 
        private void CopyFile(string originalPath, string shadowCopyPath)
        {
            var directory = Path.GetDirectoryName(shadowCopyPath);
            Directory.CreateDirectory(directory);
 
            File.Copy(originalPath, shadowCopyPath);
 
            MakeWritable(new FileInfo(shadowCopyPath));
        }
 
        private void MakeWritable(string directoryPath)
        {
            var directory = new DirectoryInfo(directoryPath);
 
            foreach (var file in directory.EnumerateFiles(searchPattern: "*", searchOption: SearchOption.AllDirectories))
            {
                MakeWritable(file);
            }
        }
 
        private void MakeWritable(FileInfo file)
        {
            try
            {
                if (file.IsReadOnly)
                {
                    file.IsReadOnly = false;
                }
            }
            catch
            {
                // There are many reasons this could fail. Ignore it and keep going.
            }
        }
 
        private void PurgeUnusedDirectories()
        {
            IEnumerable<string> directories;
            try
            {
                directories = Directory.EnumerateDirectories(BaseDirectory);
            }
            catch (DirectoryNotFoundException)
            {
                return;
            }
 
            foreach (var directory in directories)
            {
                Mutex mutex = null;
                try
                {
                    // We only want to try deleting the directory if no-one else is currently using it.
                    //
                    // Note that the mutex name is the name of the directory. This is OK because we're using
                    // GUIDs as directory/mutex names.
                    if (!Mutex.TryOpenExisting(Path.GetFileName(directory).ToLowerInvariant(), out mutex))
                    {
                        MakeWritable(directory);
                        Directory.Delete(directory, recursive: true);
                    }
                }
                catch
                {
                    // If something goes wrong we will leave it to the next run to clean up.
                    // Just swallow the exception and move on.
                }
                finally
                {
                    if (mutex != null)
                    {
                        mutex.Dispose();
                    }
                }
            }
        }
    }
}