|
// 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.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Xml;
using System.Xml.Linq;
using NuGet.Common;
namespace NuGet.Configuration
{
internal sealed class SettingsFile
{
/// <summary>
/// Full path to the settings file
/// </summary>
internal string ConfigFilePath { get; }
/// <summary>
/// Folder under which the settings file is present
/// </summary>
internal string DirectoryPath { get; }
/// <summary>
/// Filename of the settings file
/// </summary>
internal string FileName { get; }
/// <summary>
/// Defines if the configuration settings have been changed but have not been saved to disk
/// </summary>
internal bool IsDirty { get; set; }
/// <summary>
/// Defines if the settings file is considered a machine wide settings file
/// </summary>
/// <remarks>Machine wide settings files cannot be edited.</remarks>
internal bool IsMachineWide { get; }
/// <summary>
/// Determines if the settings file is considered read-only from NuGet perspective.
/// </summary>
/// <remarks>User-wide configuration files imported from non-default locations are not considered editable.
/// Note that this is different from <see cref="IsMachineWide"/>. <see cref="IsReadOnly"/> will return <see langword="true"/> for every machine-wide config. </remarks>
internal bool IsReadOnly { get; }
/// <summary>
/// XML element for settings file
/// </summary>
private readonly XDocument _xDocument;
/// <summary>
/// Root element of configuration file.
/// By definition of a nuget.config, the root element has to be a 'configuration' element
/// </summary>
private readonly NuGetConfiguration _rootElement;
/// <summary>
/// Creates an instance of a non machine wide SettingsFile with the default filename.
/// </summary>
/// <param name="directoryPath">path to the directory where the file is</param>
public SettingsFile(string directoryPath)
: this(directoryPath, Settings.DefaultSettingsFileName, isMachineWide: false, isReadOnly: false)
{
}
/// <summary>
/// Creates an instance of a non machine wide SettingsFile.
/// </summary>
/// <param name="directoryPath">path to the directory where the file is</param>
/// <param name="fileName">name of config file</param>
public SettingsFile(string directoryPath, string fileName)
: this(directoryPath, fileName, isMachineWide: false, isReadOnly: false)
{
}
/// <summary>
/// Creates an instance of a SettingsFile
/// </summary>
/// <remarks>It will parse the specified document,
/// if it doesn't exist it will create one with the default configuration.</remarks>
/// <param name="directoryPath">path to the directory where the file is</param>
/// <param name="fileName">name of config file</param>
/// <param name="isMachineWide">specifies if the SettingsFile is machine wide.</param>
/// <param name="isReadOnly">specifies if the SettingsFile is read only. If the config is machine wide, the value passed here is irrelevant. <see cref="IsReadOnly"/> will return <see langword="true"/> for every machine-wide config.</param>
public SettingsFile(string directoryPath, string fileName, bool isMachineWide, bool isReadOnly)
{
if (string.IsNullOrEmpty(directoryPath))
{
throw new ArgumentException(Resources.Argument_Cannot_Be_Null_Or_Empty, nameof(directoryPath));
}
if (string.IsNullOrEmpty(fileName))
{
throw new ArgumentException(Resources.Argument_Cannot_Be_Null_Or_Empty, nameof(fileName));
}
if (!FileSystemUtility.IsPathAFile(fileName))
{
throw new ArgumentException(Resources.Settings_FileName_Cannot_Be_A_Path, nameof(fileName));
}
DirectoryPath = directoryPath;
FileName = fileName;
ConfigFilePath = Path.GetFullPath(Path.Combine(DirectoryPath, FileName));
IsMachineWide = isMachineWide;
IsReadOnly = IsMachineWide || isReadOnly;
if (ConfigurationEventSource.Instance.IsEnabled()) ConfigurationEventSource.Instance.SettingsFile_FileReadStart(ConfigFilePath, isMachineWide ? 1 : 0, isReadOnly ? 1 : 0);
try
{
XDocument config = ExecuteSynchronized(() =>
{
return FileSystemUtility.GetOrCreateDocument(CreateDefaultConfig(), ConfigFilePath);
});
if (config.Root is null)
{
throw new InvalidOperationException("The root element of the configuration file is null.");
}
_xDocument = config;
_rootElement = new NuGetConfiguration(_xDocument.Root, origin: this);
}
finally
{
if (ConfigurationEventSource.Instance.IsEnabled()) ConfigurationEventSource.Instance.SettingsFile_FileReadStop(ConfigFilePath, isMachineWide ? 1 : 0, isReadOnly ? 1 : 0);
}
}
/// <summary>
/// Gets the section with a given name.
/// </summary>
/// <param name="sectionName">name to match sections</param>
/// <returns>null if no section with the given name was found</returns>
public SettingSection? GetSection(string sectionName)
{
return _rootElement.GetSection(sectionName);
}
/// <summary>
/// Adds or updates the given <paramref name="item"/> to the settings.
/// </summary>
/// <param name="sectionName">section where the <paramref name="item"/> has to be added. If this section does not exist, one will be created.</param>
/// <param name="item">item to be added to the settings.</param>
/// <returns>true if the item was successfully updated or added in the settings</returns>
internal void AddOrUpdate(string sectionName, SettingItem item)
{
_rootElement.AddOrUpdate(sectionName, item);
}
/// <summary>
/// Removes the given <paramref name="item"/> from the settings.
/// If the <paramref name="item"/> is the last item in the section, the section will also be removed.
/// </summary>
/// <param name="sectionName">Section where the <paramref name="item"/> is stored. If this section does not exist, the method will throw</param>
/// <param name="item">item to be removed from the settings</param>
/// <remarks> If the SettingsFile is a machine wide config this method will throw</remarks>
internal void Remove(string sectionName, SettingItem item)
{
_rootElement.Remove(sectionName, item);
}
/// <summary>
/// Flushes any in-memory updates in the SettingsFile to disk.
/// </summary>
internal void SaveToDisk()
{
if (IsDirty)
{
_ = ExecuteSynchronized(() =>
{
FileSystemUtility.AddFile(ConfigFilePath, _xDocument.Save);
return 0;
});
IsDirty = false;
}
}
internal bool IsEmpty() => _rootElement == null || _rootElement.IsEmpty();
/// <remarks>
/// This method gives you a reference to the actual abstraction instead of a clone of it.
/// It should be used only when intended. For most purposes you should be able to use
/// GetSection(...) instead.
/// </remarks>
internal bool TryGetSection(string sectionName, [NotNullWhen(true)] out SettingSection? section)
{
return _rootElement.Sections.TryGetValue(sectionName, out section);
}
internal void MergeSectionsInto(Dictionary<string, VirtualSettingSection> sectionsContainer)
{
_rootElement.MergeSectionsInto(sectionsContainer);
}
private XDocument CreateDefaultConfig()
{
var configurationElement = new NuGetConfiguration(this);
return new XDocument(configurationElement.AsXNode());
}
private T ExecuteSynchronized<T>(Func<T> ioOperation)
{
T? result = default;
ConcurrencyUtilities.ExecuteWithFileLocked(filePath: ConfigFilePath, action: () =>
{
try
{
result = ioOperation();
}
catch (InvalidOperationException e)
{
throw new NuGetConfigurationException(
string.Format(CultureInfo.CurrentCulture, Resources.ShowError_ConfigInvalidOperation, ConfigFilePath, e.Message), e);
}
catch (UnauthorizedAccessException e)
{
throw new NuGetConfigurationException(
string.Format(CultureInfo.CurrentCulture, Resources.ShowError_ConfigUnauthorizedAccess, ConfigFilePath, e.Message), e);
}
catch (XmlException e)
{
throw new NuGetConfigurationException(
string.Format(CultureInfo.CurrentCulture, Resources.ShowError_ConfigInvalidXml, ConfigFilePath, e.Message), e);
}
catch (Exception e)
{
throw new NuGetConfigurationException(
string.Format(CultureInfo.CurrentCulture, Resources.Unknown_Config_Exception, ConfigFilePath, e.Message), e);
}
});
#pragma warning disable CS8603 // Possible null reference return.
// Code isn't designed to work will nullable checks enabled.
return result;
#pragma warning restore CS8603 // Possible null reference return.
}
}
}
|