File: Plugins\PluginCacheEntry.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Protocol\NuGet.Protocol.csproj (NuGet.Protocol)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable disable

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace NuGet.Protocol.Plugins
{
    /// <summary>
    /// This class represents a plugin operations cache entry.
    /// It contains expiry logic, read/write/update logic.
    /// </summary>
    public sealed class PluginCacheEntry
    {
        /// <summary>
        /// Create a plugin cache entry.
        /// </summary>
        /// <param name="rootCacheFolder">The root cache folder, normally /localappdata/nuget/plugins-cache</param>
        /// <param name="pluginFilePath">The full plugin file path, which will be used to create a key for the folder created in the root folder itself </param>
        /// <param name="requestKey">A unique request key for the operation claims. Ideally the packageSourceRepository value of the PluginRequestKey. Example https://protected.package.feed/index.json, or Source-Agnostic</param>
        public PluginCacheEntry(string rootCacheFolder, string pluginFilePath, string requestKey)
        {
            RootFolder = Path.Combine(rootCacheFolder, CachingUtility.RemoveInvalidFileNameChars(CachingUtility.ComputeHash(pluginFilePath, addIdentifiableCharacters: false)));
            CacheFileName = Path.Combine(RootFolder, CachingUtility.RemoveInvalidFileNameChars(CachingUtility.ComputeHash(requestKey, addIdentifiableCharacters: false)) + ".dat");
            NewCacheFileName = CacheFileName + "-new";
        }

        internal TimeSpan MaxAge { get; set; } = TimeSpan.FromDays(30);
        internal string CacheFileName { get; }
        private string RootFolder { get; }
        private string NewCacheFileName { get; }

        public IReadOnlyList<OperationClaim> OperationClaims { get; set; }

        /// <summary>
        /// Loads and processes the contet from the generated file if it exists.
        /// Even after this method is invoked, the operation claims might be null. 
        /// </summary>
        public void LoadFromFile()
        {
            Stream content = null;
            try
            {
                content = CachingUtility.ReadCacheFile(MaxAge, CacheFileName);
                if (content != null)
                {
                    ProcessContent(content);
                }
            }
            finally
            {
                content?.Dispose();
            }
        }

        private void ProcessContent(Stream content)
        {
            var serializer = new JsonSerializer();
            using (var sr = new StreamReader(content))
            using (var jsonTextReader = new JsonTextReader(sr))
            {
                OperationClaims = serializer.Deserialize<IReadOnlyList<OperationClaim>>(jsonTextReader);
            }
        }

        /// <summary>
        /// Updates the cache file with the current value in the operation claims if the operationn claims is not null.
        /// </summary>
        /// <returns>Task</returns>
        public async Task UpdateCacheFileAsync()
        {
            if (OperationClaims != null)
            {
                // Make sure the cache file directory is created before writing a file to it.
                Directory.CreateDirectory(RootFolder);

                // The update of a cached file is divided into two steps:
                // 1) Delete the old file.
                // 2) Create a new file with the same name.
                using (var fileStream = new FileStream(
                    NewCacheFileName,
                    FileMode.Create,
                    FileAccess.ReadWrite,
                    FileShare.None,
                    CachingUtility.BufferSize))
                {
                    var json = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(OperationClaims, Formatting.Indented));
                    await fileStream.WriteAsync(json, 0, json.Length);
                }

                if (File.Exists(CacheFileName))
                {
                    // Process B can perform deletion on an opened file if the file is opened by process A
                    // with FileShare.Delete flag. However, the file won't be actually deleted until A close it.
                    // This special feature can cause race condition, so we never delete an opened file.
                    if (!CachingUtility.IsFileAlreadyOpen(CacheFileName))
                    {
                        File.Delete(CacheFileName);
                    }
                }

                // If the destination file doesn't exist, we can safely perform moving operation.
                // Otherwise, moving operation will fail.
                if (!File.Exists(CacheFileName))
                {
                    File.Move(
                        NewCacheFileName,
                        CacheFileName);
                }
            }
        }
    }
}