File: Utils\FileSystemService.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only
 
using System.Collections.Concurrent;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting;
 
/// <summary>
/// Default implementation of <see cref="IFileSystemService"/>.
/// </summary>
internal sealed class FileSystemService : IFileSystemService, IDisposable
{
    private readonly TempFileSystemService _tempDirectory;
    private ILogger? _logger;
    private readonly bool _preserveTempFiles;
 
    // Track allocated temp files and directories as disposable objects using path as key
    private readonly ConcurrentDictionary<string, IDisposable> _allocatedItems = new();
 
    public FileSystemService(IConfiguration configuration)
    {
        ArgumentNullException.ThrowIfNull(configuration);
 
        // Check configuration to preserve temp files for debugging
        _preserveTempFiles = configuration["ASPIRE_PRESERVE_TEMP_FILES"] is not null;
        
        _tempDirectory = new TempFileSystemService(this);
    }
 
    /// <summary>
    /// Sets the logger for this service. Called after service provider is built.
    /// </summary>
    /// <remarks>
    /// The logger cannot be injected via constructor because the FileSystemService
    /// is allocated before logging is fully initialized in the DistributedApplicationBuilder.
    /// </remarks>
    internal void SetLogger(ILogger<FileSystemService> logger)
    {
        _logger = logger;
    }
 
    /// <inheritdoc/>
    public ITempFileSystemService TempDirectory => _tempDirectory;
 
    /// <summary>
    /// Gets whether temporary files should be preserved for debugging.
    /// </summary>
    internal bool ShouldPreserveTempFiles => _preserveTempFiles;
 
    /// <summary>
    /// Gets the logger for this service, if set.
    /// </summary>
    internal ILogger? Logger => _logger;
 
    private bool _disposed;
    private readonly object _disposeLock = new();
 
    /// <summary>
    /// Tracks a temporary item for cleanup on service disposal.
    /// </summary>
    internal void TrackItem(string path, IDisposable item)
    {
        lock (_disposeLock)
        {
            if (_disposed)
            {
                throw new ObjectDisposedException(nameof(FileSystemService), "Cannot allocate temporary files after the service has been disposed.");
            }
 
            _allocatedItems.TryAdd(path, item);
        }
    }
 
    /// <summary>
    /// Removes a temporary item from tracking.
    /// </summary>
    internal void UntrackItem(string path)
    {
        _allocatedItems.TryRemove(path, out _);
    }
 
    /// <summary>
    /// Cleans up any remaining temporary files and directories.
    /// </summary>
    public void Dispose()
    {
        lock (_disposeLock)
        {
            if (_disposed)
            {
                return;
            }
 
            _disposed = true;
        }
 
        if (_preserveTempFiles)
        {
            _logger?.LogInformation("Skipping cleanup of {Count} temporary files/directories due to ASPIRE_PRESERVE_TEMP_FILES configuration", _allocatedItems.Count);
            return;
        }
 
        if (_allocatedItems.IsEmpty)
        {
            return;
        }
 
        _logger?.LogDebug("Cleaning up {Count} remaining temporary files/directories", _allocatedItems.Count);
 
        foreach (var kvp in _allocatedItems)
        {
            try
            {
                kvp.Value.Dispose();
            }
            catch (Exception ex)
            {
                _logger?.LogWarning(ex, "Failed to clean up temporary item");
            }
        }
    }
 
    /// <summary>
    /// Implementation of <see cref="ITempFileSystemService"/>.
    /// </summary>
    private sealed class TempFileSystemService : ITempFileSystemService
    {
        private readonly FileSystemService _parent;
 
        public TempFileSystemService(FileSystemService parent)
        {
            _parent = parent;
        }
 
        /// <inheritdoc/>
        public TempDirectory CreateTempSubdirectory(string? prefix = null)
        {
            var path = Directory.CreateTempSubdirectory(prefix ?? "aspire").FullName;
            var tempDir = new DefaultTempDirectory(path, _parent);
            _parent.TrackItem(path, tempDir);
            return tempDir;
        }
 
        /// <inheritdoc/>
        public TempFile CreateTempFile(string? fileName = null)
        {
            if (fileName is null)
            {
                var tempFile = Path.GetTempFileName();
                var file = new DefaultTempFile(tempFile, deleteParentDirectory: false, _parent);
                _parent.TrackItem(tempFile, file);
                return file;
            }
 
            // Create a temp subdirectory and place the named file inside it
            var tempDir = Directory.CreateTempSubdirectory("aspire").FullName;
            var filePath = Path.Combine(tempDir, fileName);
            File.Create(filePath).Dispose();
            var tempFileObj = new DefaultTempFile(filePath, deleteParentDirectory: true, _parent);
            _parent.TrackItem(filePath, tempFileObj);
            return tempFileObj;
        }
    }
 
    /// <summary>
    /// Default implementation of <see cref="TempDirectory"/>.
    /// </summary>
    private sealed class DefaultTempDirectory : TempDirectory
    {
        private readonly string _path;
        private readonly FileSystemService _parent;
        private bool _disposed;
 
        public DefaultTempDirectory(string path, FileSystemService parent)
        {
            _path = path;
            _parent = parent;
 
            _parent.Logger?.LogDebug("Allocated temporary directory: {Path}", path);
        }
 
        public override string Path => _path;
 
        public override void Dispose()
        {
            if (_disposed)
            {
                return;
            }
 
            _disposed = true;
 
            // Remove from tracking
            _parent.UntrackItem(_path);
 
            // Skip deletion if preserve flag is set
            if (_parent.ShouldPreserveTempFiles)
            {
                return;
            }
 
            try
            {
                if (Directory.Exists(_path))
                {
                    Directory.Delete(_path, recursive: true);
                    _parent.Logger?.LogDebug("Cleaned up temporary directory: {Path}", _path);
                }
            }
            catch
            {
                // Ignore errors during cleanup
            }
        }
    }
 
    /// <summary>
    /// Default implementation of <see cref="TempFile"/>.
    /// </summary>
    private sealed class DefaultTempFile : TempFile
    {
        private readonly string _path;
        private readonly bool _deleteParentDirectory;
        private readonly FileSystemService _parent;
        private bool _disposed;
 
        public DefaultTempFile(string path, bool deleteParentDirectory, FileSystemService parent)
        {
            _path = path;
            _deleteParentDirectory = deleteParentDirectory;
            _parent = parent;
 
            _parent.Logger?.LogDebug("Allocated temporary file: {Path}", path);
        }
 
        public override string Path => _path;
 
        public override void Dispose()
        {
            if (_disposed)
            {
                return;
            }
 
            _disposed = true;
 
            // Remove from tracking
            _parent.UntrackItem(_path);
 
            // Skip deletion if preserve flag is set
            if (_parent.ShouldPreserveTempFiles)
            {
                return;
            }
 
            try
            {
                if (File.Exists(_path))
                {
                    File.Delete(_path);
                    _parent.Logger?.LogDebug("Cleaned up temporary file: {Path}", _path);
                }
 
                if (_deleteParentDirectory)
                {
                    var parentDir = System.IO.Path.GetDirectoryName(_path);
                    if (parentDir is not null && Directory.Exists(parentDir))
                    {
                        Directory.Delete(parentDir, recursive: true);
                    }
                }
            }
            catch
            {
                // Ignore errors during cleanup
            }
        }
    }
}