File: Workspace\Host\PersistentStorage\IPersistentStorageConfiguration.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Storage;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Host;
 
/// <summary>
/// Configuration of the <see cref="IPersistentStorageService"/> intended to be used to override behavior in tests.
/// </summary>
internal interface IPersistentStorageConfiguration : IWorkspaceService
{
    /// <summary>
    /// Indicates that the client expects the DB to succeed at all work and that it should not ever gracefully fall over.
    /// Should not be set in normal host environments, where it is completely reasonable for things to fail
    /// (for example, if a client asks for a key that hasn't been stored yet).
    /// </summary>
    bool ThrowOnFailure { get; }
 
    string? TryGetStorageLocation(SolutionKey solutionKey);
}
 
[ExportWorkspaceService(typeof(IPersistentStorageConfiguration)), Shared]
internal sealed class DefaultPersistentStorageConfiguration : IPersistentStorageConfiguration
{
    /// <summary>
    /// Used to ensure that the path components we generate do not contain any characters that might be invalid in a
    /// path.  For example, Base64 encoding will use <c>/</c> which is something that we definitely do not want
    /// errantly added to a path.
    /// </summary>
    private static readonly ImmutableArray<char> s_invalidPathChars = [.. Path.GetInvalidPathChars(), '/'];
 
    private static readonly string s_cacheDirectory;
    private static readonly string s_moduleFileName;
 
    static DefaultPersistentStorageConfiguration()
    {
        // Store in the LocalApplicationData/Roslyn/hash folder (%appdatalocal%/... on Windows,
        // ~/.local/share/... on unix).  This will place the folder in a location we can trust
        // to be able to get back to consistently as long as we're working with the same
        // solution and the same workspace kind.
        var appDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.Create);
        s_cacheDirectory = Path.Combine(appDataFolder, "Microsoft", "VisualStudio", "Roslyn", "Cache");
        var fileName = Process.GetCurrentProcess().MainModule?.FileName;
        Contract.ThrowIfNull(fileName);
        s_moduleFileName = SafeName(fileName);
    }
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public DefaultPersistentStorageConfiguration()
    {
    }
 
    public bool ThrowOnFailure => false;
 
    public string? TryGetStorageLocation(SolutionKey solutionKey)
    {
        if (string.IsNullOrWhiteSpace(solutionKey.FilePath))
            return null;
 
        // Ensure that each unique workspace kind for any given solution has a unique
        // folder to store their data in.
 
        return Path.Combine(
            s_cacheDirectory,
            s_moduleFileName,
            SafeName(solutionKey.FilePath));
    }
 
    private static string SafeName(string fullPath)
    {
        var fileName = Path.GetFileName(fullPath);
 
        // we don't want to build too long a path.  So only take a portion of the text we started with.
        // However, we want to avoid collisions, so ensure we also append a safe short piece of text
        // that is based on the full text.
        const int MaxLength = 20;
        var prefix = fileName.Length > MaxLength ? fileName[..MaxLength] : fileName;
        var suffix = Checksum.Create(fullPath);
        var fullName = $"{prefix}-{suffix}";
        return StripInvalidPathChars(fullName);
    }
 
    private static string StripInvalidPathChars(string val)
    {
        val = new string(val.Where(c => !s_invalidPathChars.Contains(c)).ToArray());
 
        return string.IsNullOrWhiteSpace(val) ? "None" : val;
    }
}