From 135a92001b876fc4604238f5efe9305a1e5e4c91 Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Sun, 21 Jan 2018 15:02:12 -0800 Subject: [PATCH 1/2] Use a null file system for the OptimizedZipPackage's expanded file system (#48) Address https://github.com/NuGet/NuGetGallery/issues/5230 --- src/NuGet.Server.Core/Core/NullFileSystem.cs | 40 +++++ src/NuGet.Server.Core/Core/PackageFactory.cs | 29 ++++ .../Infrastructure/ServerPackageRepository.cs | 2 +- .../NuGet.Server.Core.csproj | 2 + .../Controllers/NuGetODataController.cs | 4 +- .../Core/PackageFactoryTest.cs | 83 ++++++++++ .../NuGet.Server.Core.Tests.csproj | 1 + test/NuGet.Server.Tests/IntegrationTests.cs | 146 +++++++++++++++++- 8 files changed, 300 insertions(+), 7 deletions(-) create mode 100644 src/NuGet.Server.Core/Core/NullFileSystem.cs create mode 100644 src/NuGet.Server.Core/Core/PackageFactory.cs create mode 100644 test/NuGet.Server.Core.Tests/Core/PackageFactoryTest.cs diff --git a/src/NuGet.Server.Core/Core/NullFileSystem.cs b/src/NuGet.Server.Core/Core/NullFileSystem.cs new file mode 100644 index 00000000..cda19b1b --- /dev/null +++ b/src/NuGet.Server.Core/Core/NullFileSystem.cs @@ -0,0 +1,40 @@ +// 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.IO; + +namespace NuGet.Server.Core +{ + /// + /// A file system implementation that persists nothing. This is intended to be used with + /// so that package files are never actually extracted anywhere on disk. + /// + public class NullFileSystem : IFileSystem + { + public static NullFileSystem Instance { get; } = new NullFileSystem(); + + public Stream CreateFile(string path) => Stream.Null; + public bool DirectoryExists(string path) => true; + public bool FileExists(string path) => false; + public string GetFullPath(string path) => null; + public Stream OpenFile(string path) => Stream.Null; + + public ILogger Logger { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + public string Root => throw new NotSupportedException(); + public void AddFile(string path, Stream stream) => throw new NotSupportedException(); + public void AddFile(string path, Action writeToStream) => throw new NotSupportedException(); + public void AddFiles(IEnumerable files, string rootDir) => throw new NotSupportedException(); + public void DeleteDirectory(string path, bool recursive) => throw new NotSupportedException(); + public void DeleteFile(string path) => throw new NotSupportedException(); + public void DeleteFiles(IEnumerable files, string rootDir) => throw new NotSupportedException(); + public DateTimeOffset GetCreated(string path) => throw new NotSupportedException(); + public IEnumerable GetDirectories(string path) => throw new NotSupportedException(); + public IEnumerable GetFiles(string path, string filter, bool recursive) => throw new NotSupportedException(); + public DateTimeOffset GetLastAccessed(string path) => throw new NotSupportedException(); + public DateTimeOffset GetLastModified(string path) => throw new NotSupportedException(); + public void MakeFileWritable(string path) => throw new NotSupportedException(); + public void MoveFile(string source, string destination) => throw new NotSupportedException(); + } +} diff --git a/src/NuGet.Server.Core/Core/PackageFactory.cs b/src/NuGet.Server.Core/Core/PackageFactory.cs new file mode 100644 index 00000000..59a3eb0b --- /dev/null +++ b/src/NuGet.Server.Core/Core/PackageFactory.cs @@ -0,0 +1,29 @@ +// 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.IO; + +namespace NuGet.Server.Core +{ + public static class PackageFactory + { + public static IPackage Open(string fullPackagePath) + { + if (string.IsNullOrEmpty(fullPackagePath)) + { + throw new ArgumentNullException(nameof(fullPackagePath)); + } + + var directoryName = Path.GetDirectoryName(fullPackagePath); + var fileName = Path.GetFileName(fullPackagePath); + + var fileSystem = new PhysicalFileSystem(directoryName); + + return new OptimizedZipPackage( + fileSystem, + fileName, + NullFileSystem.Instance); + } + } +} diff --git a/src/NuGet.Server.Core/Infrastructure/ServerPackageRepository.cs b/src/NuGet.Server.Core/Infrastructure/ServerPackageRepository.cs index 04d17887..38aa495c 100644 --- a/src/NuGet.Server.Core/Infrastructure/ServerPackageRepository.cs +++ b/src/NuGet.Server.Core/Infrastructure/ServerPackageRepository.cs @@ -236,7 +236,7 @@ private void AddPackagesFromDropFolderWithoutLocking() try { // Create package - var package = new OptimizedZipPackage(_fileSystem, packageFile); + var package = PackageFactory.Open(_fileSystem.GetFullPath(packageFile)); if (!CanPackageBeAddedWithoutLocking(package, shouldThrow: false)) { diff --git a/src/NuGet.Server.Core/NuGet.Server.Core.csproj b/src/NuGet.Server.Core/NuGet.Server.Core.csproj index 07c3a268..3f7cbb12 100644 --- a/src/NuGet.Server.Core/NuGet.Server.Core.csproj +++ b/src/NuGet.Server.Core/NuGet.Server.Core.csproj @@ -58,7 +58,9 @@ + + diff --git a/src/NuGet.Server.V2/Controllers/NuGetODataController.cs b/src/NuGet.Server.V2/Controllers/NuGetODataController.cs index f7ce0dbd..03c7d745 100644 --- a/src/NuGet.Server.V2/Controllers/NuGetODataController.cs +++ b/src/NuGet.Server.V2/Controllers/NuGetODataController.cs @@ -14,6 +14,7 @@ using System.Web.Http; using System.Web.Http.OData; using System.Web.Http.OData.Query; +using NuGet.Server.Core; using NuGet.Server.Core.DataServices; using NuGet.Server.Core.Infrastructure; using NuGet.Server.V2.Model; @@ -396,8 +397,7 @@ public virtual async Task UploadPackage(CancellationToken t } } - var package = new OptimizedZipPackage(temporaryFile); - + var package = PackageFactory.Open(temporaryFile); HttpResponseMessage retValue; if (_authenticationService.IsAuthenticated(User, apiKey, package.Id)) diff --git a/test/NuGet.Server.Core.Tests/Core/PackageFactoryTest.cs b/test/NuGet.Server.Core.Tests/Core/PackageFactoryTest.cs new file mode 100644 index 00000000..22364fb0 --- /dev/null +++ b/test/NuGet.Server.Core.Tests/Core/PackageFactoryTest.cs @@ -0,0 +1,83 @@ +// 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.IO; +using NuGet.Server.Core.Tests.Infrastructure; +using Xunit; + +namespace NuGet.Server.Core.Tests.Core +{ + public class PackageFactoryTest + { + public class Open : IDisposable + { + private readonly TemporaryDirectory _directory; + + public Open() + { + _directory = new TemporaryDirectory(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void RejectsInvalidPaths(string path) + { + var ex = Assert.Throws( + () => PackageFactory.Open(path)); + Assert.Equal("fullPackagePath", ex.ParamName); + } + + [Fact] + public void InitializesPackageWithMetadata() + { + // Arrange + var path = Path.Combine(_directory, "package.nupkg"); + TestData.CopyResourceToPath(TestData.PackageResource, path); + + // Act + var package = PackageFactory.Open(path); + + // Assert + Assert.Equal(TestData.PackageId, package.Id); + Assert.Equal(TestData.PackageVersion, package.Version); + } + + [Fact] + public void InitializesPackageWithSupportedFrameworks() + { + // Arrange + var path = Path.Combine(_directory, "package.nupkg"); + TestData.CopyResourceToPath(TestData.PackageResource, path); + + // Act + var package = PackageFactory.Open(path); + + // Assert + var frameworks = package.GetSupportedFrameworks(); + var framework = Assert.Single(frameworks); + Assert.Equal(VersionUtility.ParseFrameworkName("net40-client"), framework); + } + + [Fact] + public void InitializesPackageWhichCanBeCheckedForSymbols() + { + // Arrange + var path = Path.Combine(_directory, "package.nupkg"); + TestData.CopyResourceToPath(TestData.PackageResource, path); + + // Act + var package = PackageFactory.Open(path); + + // Assert + Assert.False(package.IsSymbolsPackage(), "The provided package is not a symbols package."); + } + + public void Dispose() + { + _directory?.Dispose(); + } + } + } +} diff --git a/test/NuGet.Server.Core.Tests/NuGet.Server.Core.Tests.csproj b/test/NuGet.Server.Core.Tests/NuGet.Server.Core.Tests.csproj index 1d7d70b6..3bac6abd 100644 --- a/test/NuGet.Server.Core.Tests/NuGet.Server.Core.Tests.csproj +++ b/test/NuGet.Server.Core.Tests/NuGet.Server.Core.Tests.csproj @@ -81,6 +81,7 @@ + diff --git a/test/NuGet.Server.Tests/IntegrationTests.cs b/test/NuGet.Server.Tests/IntegrationTests.cs index 5297d021..312d5008 100644 --- a/test/NuGet.Server.Tests/IntegrationTests.cs +++ b/test/NuGet.Server.Tests/IntegrationTests.cs @@ -6,13 +6,16 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.IO; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Threading; using System.Threading.Tasks; using System.Web.Http; using System.Web.Http.Dependencies; using NuGet.Server.App_Start; +using NuGet.Server.Core.Infrastructure; using NuGet.Server.Core.Tests; using NuGet.Server.Core.Tests.Infrastructure; using Xunit; @@ -56,6 +59,72 @@ public async Task DropPackageThenReadPackages() } } + [Fact] + public async Task DownloadPackage() + { + // Arrange + using (var tc = new TestContext()) + { + // Act & Assert + // 1. Write a package to the drop folder. + var packagePath = Path.Combine(tc.PackagesDirectory, "package.nupkg"); + TestData.CopyResourceToPath(TestData.PackageResource, packagePath); + var expectedBytes = File.ReadAllBytes(packagePath); + + // 2. Download the package. + using (var request = new HttpRequestMessage( + HttpMethod.Get, + $"/nuget/Packages(Id='{TestData.PackageId}',Version='{TestData.PackageVersionString}')/Download")) + using (var response = await tc.Client.SendAsync(request)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var actualBytes = await response.Content.ReadAsByteArrayAsync(); + + Assert.Equal("binary/octet-stream", response.Content.Headers.ContentType.ToString()); + Assert.Equal(expectedBytes, actualBytes); + } + } + } + + [Fact] + public async Task FilterOnFramework() + { + // Arrange + using (var tc = new TestContext()) + { + tc.Settings["enableFrameworkFiltering"] = "true"; + + // Act & Assert + // 1. Write a package to the drop folder. + var packagePath = Path.Combine(tc.PackagesDirectory, "package.nupkg"); + TestData.CopyResourceToPath(TestData.PackageResource, packagePath); + + // 2. Search for all packages supporting .NET Framework 4.6 (this should match the test package) + using (var request = new HttpRequestMessage( + HttpMethod.Get, + $"/nuget/Search?targetFramework='net46'")) + using (var response = await tc.Client.SendAsync(request)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + + Assert.Contains(TestData.PackageId, content); + } + + // 3. Search for all packages supporting .NET Framework 2.0 (this should match nothing) + using (var request = new HttpRequestMessage( + HttpMethod.Get, + $"/nuget/Search?targetFramework='net20'")) + using (var response = await tc.Client.SendAsync(request)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + + Assert.DoesNotContain(TestData.PackageId, content); + } + } + } + [Fact] public async Task PushPackageThenReadPackages() { @@ -122,6 +191,75 @@ public async Task CanQueryUsingProjection(string endpoint) } } + [Fact] + public async Task DoesNotWriteToNuGetScratch() + { + // Arrange + OptimizedZipPackage.PurgeCache(); + var expectedTempEntries = Directory + .GetFileSystemEntries(Path.Combine(Path.GetTempPath(), "NuGetScratch")) + .OrderBy(x => x) + .ToList(); + + using (var tc = new TestContext()) + { + tc.Settings["enableFrameworkFiltering"] = "true"; + tc.Settings["allowOverrideExistingPackageOnPush"] = "true"; + + string apiKey = "foobar"; + tc.SetApiKey(apiKey); + + // Act & Assert + // 1. Write a package to the drop folder. + var packagePath = Path.Combine(tc.PackagesDirectory, "package.nupkg"); + TestData.CopyResourceToPath(TestData.PackageResource, packagePath); + + // 2. Search for packages. + using (var request = new HttpRequestMessage( + HttpMethod.Get, + $"/nuget/Search?targetFramework='net46'")) + using (var response = await tc.Client.SendAsync(request)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + // 3. Push the package. + var pushPath = Path.Combine(tc.TemporaryDirectory, "package.nupkg"); + TestData.CopyResourceToPath(TestData.PackageResource, pushPath); + using (var request = new HttpRequestMessage(HttpMethod.Put, "/nuget") + { + Headers = + { + { "X-NUGET-APIKEY", apiKey } + }, + Content = tc.GetFileUploadContent(pushPath), + }) + { + using (request) + using (var response = await tc.Client.SendAsync(request)) + { + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + } + } + + // 4. Search for packages again. + using (var request = new HttpRequestMessage( + HttpMethod.Get, + $"/nuget/Search?targetFramework='net46'")) + using (var response = await tc.Client.SendAsync(request)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + // 6. Make sure we have not added more temp files. + var actualTempEntries = Directory + .GetFileSystemEntries(Path.Combine(Path.GetTempPath(), "NuGetScratch")) + .OrderBy(x => x) + .ToList(); + Assert.Equal(expectedTempEntries, actualTempEntries); + } + } + public static IEnumerable EndpointsSupportingProjection { get @@ -135,7 +273,6 @@ public static IEnumerable EndpointsSupportingProjection private sealed class TestContext : IDisposable { private readonly HttpServer _server; - private readonly DefaultServiceResolver _serviceResolver; private readonly HttpConfiguration _config; public TestContext() @@ -149,11 +286,11 @@ public TestContext() { "apiKey", string.Empty } }; - _serviceResolver = new DefaultServiceResolver(PackagesDirectory, Settings); + ServiceResolver = new DefaultServiceResolver(PackagesDirectory, Settings); _config = new HttpConfiguration(); _config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always; - _config.DependencyResolver = new DependencyResolverAdapter(_serviceResolver); + _config.DependencyResolver = new DependencyResolverAdapter(ServiceResolver); NuGetODataConfig.Initialize(_config, "TestablePackagesOData"); @@ -162,6 +299,7 @@ public TestContext() Client.BaseAddress = new Uri("http://localhost/"); } + public DefaultServiceResolver ServiceResolver { get; } public TemporaryDirectory TemporaryDirectory { get; } public TemporaryDirectory PackagesDirectory { get; } public NameValueCollection Settings { get; } @@ -198,7 +336,7 @@ public void Dispose() Client.Dispose(); _server.Dispose(); _config.Dispose(); - _serviceResolver.Dispose(); + ServiceResolver.Dispose(); PackagesDirectory.Dispose(); TemporaryDirectory.Dispose(); } From 44ce5767383e048bd83605d7af3d810aa32a6d93 Mon Sep 17 00:00:00 2001 From: Tarjei Olsen Date: Sat, 3 Feb 2018 18:27:44 +0100 Subject: [PATCH 2/2] Configurable cache file name (#49) * Added support for custom cache file name. Handy when you have configured two feeds pointing to the same packages folder. * added some air after commas in type parameter * added curlies, even on one liners * renamed misleading InitializeServerPackageStore method to InitializeServerPackageCache * Updated XML comment explaining the custom cache file name option * Throwing up on invalid cache file name. Added tests to prove it. * compacting test arrange code --- .../DictionarySettingsProvider.cs | 12 ++- .../Program.cs | 2 +- .../Infrastructure/DefaultSettingsProvider.cs | 5 ++ .../Infrastructure/ISettingsProvider.cs | 1 + .../Infrastructure/ServerPackageRepository.cs | 38 +++++++- src/NuGet.Server.Core/Strings.Designer.cs | 9 ++ src/NuGet.Server.Core/Strings.resx | 3 + .../WebConfigSettingsProvider.cs | 6 ++ src/NuGet.Server/Web.config | 6 ++ .../Infrastructure/FuncSettingsProvider.cs | 11 ++- .../ServerPackageRepositoryTest.cs | 89 +++++++++++++++++-- 11 files changed, 165 insertions(+), 17 deletions(-) diff --git a/samples/NuGet.Server.V2.Samples.OwinHost/DictionarySettingsProvider.cs b/samples/NuGet.Server.V2.Samples.OwinHost/DictionarySettingsProvider.cs index 8958501b..1bc49c90 100644 --- a/samples/NuGet.Server.V2.Samples.OwinHost/DictionarySettingsProvider.cs +++ b/samples/NuGet.Server.V2.Samples.OwinHost/DictionarySettingsProvider.cs @@ -1,5 +1,6 @@ // 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 NuGet.Server.Core.Infrastructure; @@ -7,9 +8,9 @@ namespace NuGet.Server.V2.Samples.OwinHost { public class DictionarySettingsProvider : ISettingsProvider { - readonly Dictionary _settings; + readonly Dictionary _settings; - public DictionarySettingsProvider(Dictionary settings) + public DictionarySettingsProvider(Dictionary settings) { _settings = settings; } @@ -18,8 +19,13 @@ public DictionarySettingsProvider(Dictionary settings) public bool GetBoolSetting(string key, bool defaultValue) { System.Diagnostics.Debug.WriteLine("getSetting: " + key); - return _settings.ContainsKey(key) ? _settings[key] : defaultValue; + return _settings.ContainsKey(key) ? Convert.ToBoolean(_settings[key]) : defaultValue; } + + public string GetStringSetting(string key, string defaultValue) + { + return _settings.ContainsKey(key) ? Convert.ToString(_settings[key]) : defaultValue; + } } } diff --git a/samples/NuGet.Server.V2.Samples.OwinHost/Program.cs b/samples/NuGet.Server.V2.Samples.OwinHost/Program.cs index 6f952bd3..46e775f6 100644 --- a/samples/NuGet.Server.V2.Samples.OwinHost/Program.cs +++ b/samples/NuGet.Server.V2.Samples.OwinHost/Program.cs @@ -24,7 +24,7 @@ static void Main(string[] args) // Set up a common settingsProvider to be used by all repositories. // If a setting is not present in dictionary default value will be used. - var settings = new Dictionary(); + var settings = new Dictionary(); settings.Add("enableDelisting", false); //default=false settings.Add("enableFrameworkFiltering", false); //default=false settings.Add("ignoreSymbolsPackages", true); //default=false diff --git a/src/NuGet.Server.Core/Infrastructure/DefaultSettingsProvider.cs b/src/NuGet.Server.Core/Infrastructure/DefaultSettingsProvider.cs index 65f31e12..b3c5ce97 100644 --- a/src/NuGet.Server.Core/Infrastructure/DefaultSettingsProvider.cs +++ b/src/NuGet.Server.Core/Infrastructure/DefaultSettingsProvider.cs @@ -8,5 +8,10 @@ public bool GetBoolSetting(string key, bool defaultValue) { return defaultValue; } + + public string GetStringSetting(string key, string defaultValue) + { + return defaultValue; + } } } diff --git a/src/NuGet.Server.Core/Infrastructure/ISettingsProvider.cs b/src/NuGet.Server.Core/Infrastructure/ISettingsProvider.cs index bd20aa57..6fca200a 100644 --- a/src/NuGet.Server.Core/Infrastructure/ISettingsProvider.cs +++ b/src/NuGet.Server.Core/Infrastructure/ISettingsProvider.cs @@ -5,5 +5,6 @@ namespace NuGet.Server.Core.Infrastructure public interface ISettingsProvider { bool GetBoolSetting(string key, bool defaultValue); + string GetStringSetting(string key, string defaultValue); } } diff --git a/src/NuGet.Server.Core/Infrastructure/ServerPackageRepository.cs b/src/NuGet.Server.Core/Infrastructure/ServerPackageRepository.cs index 38aa495c..6dcd9c61 100644 --- a/src/NuGet.Server.Core/Infrastructure/ServerPackageRepository.cs +++ b/src/NuGet.Server.Core/Infrastructure/ServerPackageRepository.cs @@ -58,7 +58,7 @@ public ServerPackageRepository( _runBackgroundTasks = true; _settingsProvider = settingsProvider ?? new DefaultSettingsProvider(); _logger = logger ?? new TraceLogger(); - _serverPackageCache = InitializeServerPackageStore(); + _serverPackageCache = InitializeServerPackageCache(); _serverPackageStore = new ServerPackageStore( _fileSystem, new ExpandedPackageRepository(_fileSystem, hashProvider), @@ -81,7 +81,7 @@ internal ServerPackageRepository( _runBackgroundTasks = runBackgroundTasks; _settingsProvider = settingsProvider ?? new DefaultSettingsProvider(); _logger = logger ?? new TraceLogger(); - _serverPackageCache = InitializeServerPackageStore(); + _serverPackageCache = InitializeServerPackageCache(); _serverPackageStore = new ServerPackageStore( _fileSystem, innerRepository, @@ -105,9 +105,39 @@ internal ServerPackageRepository( private bool EnableFileSystemMonitoring => _settingsProvider.GetBoolSetting("enableFileSystemMonitoring", true); - private ServerPackageCache InitializeServerPackageStore() + private string CacheFileName => _settingsProvider.GetStringSetting("cacheFileName", null); + + private ServerPackageCache InitializeServerPackageCache() { - return new ServerPackageCache(_fileSystem, Environment.MachineName.ToLowerInvariant() + ".cache.bin"); + return new ServerPackageCache(_fileSystem, ResolveCacheFileName()); + } + + private string ResolveCacheFileName() + { + var fileName = CacheFileName; + const string suffix = ".cache.bin"; + + if (String.IsNullOrWhiteSpace(fileName)) + { + // Default file name + return Environment.MachineName.ToLowerInvariant() + suffix; + } + + if (fileName.LastIndexOfAny(Path.GetInvalidFileNameChars()) > 0) + { + var message = string.Format(Strings.Error_InvalidCacheFileName, fileName); + + _logger.Log(LogLevel.Error, message); + + throw new InvalidOperationException(message); + } + + if (fileName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) + { + return fileName; + } + + return fileName + suffix; } /// diff --git a/src/NuGet.Server.Core/Strings.Designer.cs b/src/NuGet.Server.Core/Strings.Designer.cs index 687e05d0..7aaf25bc 100644 --- a/src/NuGet.Server.Core/Strings.Designer.cs +++ b/src/NuGet.Server.Core/Strings.Designer.cs @@ -60,6 +60,15 @@ internal Strings() { } } + /// + /// Looks up a localized string similar to Configured cache file name '{0}' is invalid. Keep it simple; No paths allowed.. + /// + internal static string Error_InvalidCacheFileName { + get { + return ResourceManager.GetString("Error_InvalidCacheFileName", resourceCulture); + } + } + /// /// Looks up a localized string similar to Package {0} already exists. The server is configured to not allow overwriting packages that already exist.. /// diff --git a/src/NuGet.Server.Core/Strings.resx b/src/NuGet.Server.Core/Strings.resx index f3e2b0b4..9b101590 100644 --- a/src/NuGet.Server.Core/Strings.resx +++ b/src/NuGet.Server.Core/Strings.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Configured cache file name '{0}' is invalid. Keep it simple; No paths allowed. + Package {0} already exists. The server is configured to not allow overwriting packages that already exist. diff --git a/src/NuGet.Server/Infrastructure/WebConfigSettingsProvider.cs b/src/NuGet.Server/Infrastructure/WebConfigSettingsProvider.cs index 658cf083..0e524950 100644 --- a/src/NuGet.Server/Infrastructure/WebConfigSettingsProvider.cs +++ b/src/NuGet.Server/Infrastructure/WebConfigSettingsProvider.cs @@ -28,5 +28,11 @@ public bool GetBoolSetting(string key, bool defaultValue) bool value; return !bool.TryParse(settings[key], out value) ? defaultValue : value; } + + public string GetStringSetting(string key, string defaultValue) + { + var settings = _getSettings(); + return settings[key] ?? defaultValue; + } } } \ No newline at end of file diff --git a/src/NuGet.Server/Web.config b/src/NuGet.Server/Web.config index b084c2ce..6be09215 100644 --- a/src/NuGet.Server/Web.config +++ b/src/NuGet.Server/Web.config @@ -22,6 +22,12 @@ --> + + + diff --git a/test/NuGet.Server.Core.Tests/Infrastructure/FuncSettingsProvider.cs b/test/NuGet.Server.Core.Tests/Infrastructure/FuncSettingsProvider.cs index 87ebf8c2..b18397ed 100644 --- a/test/NuGet.Server.Core.Tests/Infrastructure/FuncSettingsProvider.cs +++ b/test/NuGet.Server.Core.Tests/Infrastructure/FuncSettingsProvider.cs @@ -7,8 +7,8 @@ namespace NuGet.Server.Core.Tests.Infrastructure { class FuncSettingsProvider : ISettingsProvider { - readonly Func _getSetting; - internal FuncSettingsProvider(Func getSetting) + readonly Func _getSetting; + internal FuncSettingsProvider(Func getSetting) { if (getSetting == null) { @@ -20,7 +20,12 @@ internal FuncSettingsProvider(Func getSetting) public bool GetBoolSetting(string key, bool defaultValue) { - return _getSetting(key, defaultValue); + return Convert.ToBoolean(_getSetting(key, defaultValue)); + } + + public string GetStringSetting(string key, string defaultValue) + { + return Convert.ToString(_getSetting(key, defaultValue)); } } } diff --git a/test/NuGet.Server.Core.Tests/ServerPackageRepositoryTest.cs b/test/NuGet.Server.Core.Tests/ServerPackageRepositoryTest.cs index 6fec5230..b813d942 100644 --- a/test/NuGet.Server.Core.Tests/ServerPackageRepositoryTest.cs +++ b/test/NuGet.Server.Core.Tests/ServerPackageRepositoryTest.cs @@ -23,7 +23,7 @@ public class ServerPackageRepositoryTest public static async Task CreateServerPackageRepositoryAsync( string path, Action setupRepository = null, - Func getSetting = null) + Func getSetting = null) { var fileSystem = new PhysicalFileSystem(path); var expandedPackageRepository = new ExpandedPackageRepository(fileSystem); @@ -514,7 +514,7 @@ public async Task ServerPackageRepositorySemVer1IsAbsoluteLatest(bool enableDeli using (var temporaryDirectory = new TemporaryDirectory()) { // Arrange - var getSetting = enableDelisting ? EnableDelisting : (Func)null; + var getSetting = enableDelisting ? EnableDelisting : (Func)null; var serverRepository = await CreateServerPackageRepositoryAsync(temporaryDirectory.Path, repository => { repository.AddPackage(CreatePackage("test", "2.0-alpha")); @@ -548,7 +548,7 @@ public async Task ServerPackageRepositorySemVer2IsAbsoluteLatest(bool enableDeli using (var temporaryDirectory = new TemporaryDirectory()) { // Arrange - var getSetting = enableDelisting ? EnableDelisting : (Func)null; + var getSetting = enableDelisting ? EnableDelisting : (Func)null; var serverRepository = await CreateServerPackageRepositoryAsync(temporaryDirectory.Path, repository => { repository.AddPackage(CreatePackage("test", "2.0-alpha")); @@ -603,7 +603,7 @@ public async Task ServerPackageRepositorySemVer1IsLatest(bool enableDelisting) using (var temporaryDirectory = new TemporaryDirectory()) { // Arrange - var getSetting = enableDelisting ? EnableDelisting : (Func)null; + var getSetting = enableDelisting ? EnableDelisting : (Func)null; var serverRepository = await CreateServerPackageRepositoryAsync(temporaryDirectory.Path, repository => { repository.AddPackage(CreatePackage("test1", "1.0.0")); @@ -634,7 +634,7 @@ public async Task ServerPackageRepositorySemVer2IsLatest(bool enableDelisting) using (var temporaryDirectory = new TemporaryDirectory()) { // Arrange - var getSetting = enableDelisting ? EnableDelisting : (Func)null; + var getSetting = enableDelisting ? EnableDelisting : (Func)null; var serverRepository = await CreateServerPackageRepositoryAsync(temporaryDirectory.Path, repository => { repository.AddPackage(CreatePackage("test", "1.11")); @@ -811,6 +811,83 @@ public async Task ServerPackageRepositoryAddPackageRejectsDuplicatesWithSemVer2( } } + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task ServerPackageRepository_CustomCacheFileNameNotConfigured_UseMachineNameAsFileName(string fileNameFromConfig) + { + using (var temporaryDirectory = new TemporaryDirectory()) + { + ServerPackageRepository serverRepository = await CreateServerPackageRepositoryAsync( + temporaryDirectory.Path, + getSetting: (key, defaultValue) => key == "cacheFileName" ? fileNameFromConfig : defaultValue); + + string expectedCacheFileName = Path.Combine(serverRepository.Source, Environment.MachineName.ToLowerInvariant() + ".cache.bin"); + + Assert.True(File.Exists(expectedCacheFileName)); + } + } + + [Fact] + public async Task ServerPackageRepository_CustomCacheFileNameIsConfigured_CustomCacheFileIsCreated() + { + using (var temporaryDirectory = new TemporaryDirectory()) + { + ServerPackageRepository serverRepository = await CreateServerPackageRepositoryAsync( + temporaryDirectory.Path, + getSetting: (key, defaultValue) => key == "cacheFileName" ? "CustomFileName.cache.bin" : defaultValue); + + string expectedCacheFileName = Path.Combine(serverRepository.Source, "CustomFileName.cache.bin"); + + Assert.True(File.Exists(expectedCacheFileName)); + } + } + + [Fact] + public async Task ServerPackageRepository_CustomCacheFileNameWithoutExtensionIsConfigured_CustomCacheFileWithExtensionIsCreated() + { + using (var temporaryDirectory = new TemporaryDirectory()) + { + ServerPackageRepository serverRepository = await CreateServerPackageRepositoryAsync( + temporaryDirectory.Path, + getSetting: (key, defaultValue) => key == "cacheFileName" ? "CustomFileName" : defaultValue); + + string expectedCacheFileName = Path.Combine(serverRepository.Source, "CustomFileName.cache.bin"); + + Assert.True(File.Exists(expectedCacheFileName)); + } + } + + [Theory] + [InlineData("c:\\file\\is\\a\\path\\to\\Awesome.cache.bin")] + [InlineData("random:invalidFileName.cache.bin")] + public async Task ServerPackageRepository_CustomCacheFileNameIsInvalid_ThrowUp(string invlaidCacheFileName) + { + using (var temporaryDirectory = new TemporaryDirectory()) + { + Task Code() => CreateServerPackageRepositoryAsync( + temporaryDirectory.Path, + getSetting: (key, defaultValue) => key == "cacheFileName" ? invlaidCacheFileName : defaultValue); + + await Assert.ThrowsAsync(Code); + } + } + + [Fact] + public async Task ServerPackageRepository_CustomCacheFileNameIsInvalid_ThrowUpWithCorrectErrorMessage() + { + using (var temporaryDirectory = new TemporaryDirectory()) + { + Task Code() => CreateServerPackageRepositoryAsync( + temporaryDirectory.Path, + getSetting: (key, defaultValue) => key == "cacheFileName" ? "foo:bar/baz" : defaultValue); + + var expectedMessage = "Configured cache file name 'foo:bar/baz' is invalid. Keep it simple; No paths allowed."; + Assert.Equal(expectedMessage, (await Assert.ThrowsAsync(Code)).Message); + } + } + private static IPackage CreateMockPackage(string id, string version) { var package = new Mock(); @@ -879,7 +956,7 @@ private IPackage CreatePackage( return outputPackage; } - private static bool EnableDelisting(string key, bool defaultValue) + private static object EnableDelisting(string key, object defaultValue) { if (key == "enableDelisting") {