File: src\Shared\VolumeMountPathResolver.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.
 
using System.IO.Hashing;
using System.Text;
using Aspire.Hosting.ApplicationModel;
 
namespace Aspire.Hosting;
 
/// <summary>
/// Resolves workload-scoped host directories for volume mounts used by local processes.
/// </summary>
internal static class VolumeMountPathResolver
{
    internal static string GetOrCreateLocalPath(IAspireStore store, IResource resource, string volumeName)
    {
        var path = GetLocalPath(store, resource, volumeName);
        Directory.CreateDirectory(path);
        return path;
    }
 
    internal static string GetLocalPath(IAspireStore store, IResource resource, string volumeName)
    {
        ArgumentNullException.ThrowIfNull(store);
        ArgumentNullException.ThrowIfNull(resource);
        ArgumentException.ThrowIfNullOrEmpty(volumeName);
 
        // Generic target publishers do not share volume identity consistently, so local process
        // storage is scoped to the workload. First-class target volume resources can supply a
        // target-specific resolver when they intentionally model shared storage.
        return GetPathUnderStore(
            store,
            "volumes",
            GetStablePathSegment(resource.Name),
            GetStablePathSegment(volumeName));
    }
 
    internal static string GetStablePathSegment(string value)
    {
        ArgumentException.ThrowIfNullOrEmpty(value);
 
        // Hash the complete identity instead of sanitizing it. Sanitization can collapse distinct
        // names and can still produce platform-reserved file names such as CON on Windows.
        return Convert.ToHexString(XxHash3.Hash(Encoding.UTF8.GetBytes(value))).ToLowerInvariant();
    }
 
    internal static string GetPathUnderStore(IAspireStore store, params string[] pathSegments)
    {
        ArgumentNullException.ThrowIfNull(store);
        ArgumentNullException.ThrowIfNull(pathSegments);
 
        // Every caller passes either a literal segment or GetStablePathSegment output, so the
        // combined path is always contained within the store and needs no traversal guard.
        return Path.GetFullPath(Path.Combine([Path.GetFullPath(store.BasePath), .. pathSegments]));
    }
}