diff --git a/.nuget/packages.config b/.nuget/packages.config
index 4bbca7b..47d6c8e 100644
--- a/.nuget/packages.config
+++ b/.nuget/packages.config
@@ -1,4 +1,5 @@
+
\ No newline at end of file
diff --git a/NuGet.Settings.targets b/NuGet.Settings.targets
index 92d33eb..cb8b5b8 100644
--- a/NuGet.Settings.targets
+++ b/NuGet.Settings.targets
@@ -4,7 +4,7 @@
true
$(DefineConstants);MONO;
11.0
-
+
@@ -18,18 +18,17 @@
Properties
512
-
prompt
4
true
$(DefineConstants);TRACE
$(NuGetRoot)\Extended.Settings.targets
-
+
Microsoft.VisualStudio.Shell.10.0, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL
..\..\lib\VS10\Microsoft.VisualStudio.Shell.10.0.dll
-
+
Microsoft.Build
Microsoft.VisualStudio.VCProjectEngine, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
..\..\lib\Microsoft.VisualStudio.VCProjectEngine.dll
@@ -41,7 +40,7 @@
Microsoft.VisualStudio.VCProjectEngine, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
-
+
Microsoft.VisualStudio.Shell.12.0, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL
..\..\lib\VS12\Microsoft.VisualStudio.Shell.12.0.dll
@@ -108,7 +107,7 @@
bin\Coverage\
true
- $(DefineConstants);CODE_COVERAGE
+ $(DefineConstants);CODE_COVERAGE
diff --git a/NuGet.config b/NuGet.config
index 6fd27e4..5f7414d 100644
--- a/NuGet.config
+++ b/NuGet.config
@@ -9,7 +9,7 @@
-
+
diff --git a/build.ps1 b/build.ps1
index 400c31e..eae1de6 100644
--- a/build.ps1
+++ b/build.ps1
@@ -9,9 +9,11 @@ param (
[string]$SemanticVersion = '1.0.0-zlocal',
[string]$Branch,
[string]$CommitSHA,
- [string]$BuildBranch = '1c479a7381ebbc0fe1fded765de70d513b8bd68e'
+ [string]$BuildBranch = 'cb2b9e41b18cb77ee644a51951d8c8f24cde9adf'
)
+$msBuildVersion = 15;
+
# For TeamCity - If any issue occurs, this script fail the build. - By default, TeamCity returns an exit code of 0 for all powershell scripts, even if they fail
trap {
Write-Host "BUILD FAILED: $_" -ForegroundColor Red
@@ -24,7 +26,11 @@ trap {
if (-not (Test-Path "$PSScriptRoot/build")) {
New-Item -Path "$PSScriptRoot/build" -ItemType "directory"
}
-wget -UseBasicParsing -Uri "https://raw.githubusercontent.com/NuGet/ServerCommon/$BuildBranch/build/init.ps1" -OutFile "$PSScriptRoot/build/init.ps1"
+
+# Enable TLS 1.2 since GitHub requires it.
+[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
+
+Invoke-WebRequest -UseBasicParsing -Uri "https://raw.githubusercontent.com/NuGet/ServerCommon/$BuildBranch/build/init.ps1" -OutFile "$PSScriptRoot/build/init.ps1"
. "$PSScriptRoot/build/init.ps1" -BuildBranch "$BuildBranch"
Write-Host ("`r`n" * 3)
@@ -69,14 +75,24 @@ Invoke-BuildStep 'Set version metadata in AssemblyInfo.cs' {
Invoke-BuildStep 'Building solution' {
$SolutionPath = Join-Path $PSScriptRoot "NuGet.Server.sln"
- Build-Solution $Configuration $BuildNumber -MSBuildVersion "14" $SolutionPath -SkipRestore:$SkipRestore `
+ Build-Solution $Configuration $BuildNumber -MSBuildVersion "$msBuildVersion" $SolutionPath -SkipRestore:$SkipRestore `
} `
-ev +BuildErrors
Invoke-BuildStep 'Creating artifacts' {
- New-Package (Join-Path $PSScriptRoot "src\NuGet.Server.Core\NuGet.Server.Core.csproj") -Configuration $Configuration -Symbols -BuildNumber $BuildNumber -Version $SemanticVersion -Branch $Branch
- New-Package (Join-Path $PSScriptRoot "src\NuGet.Server.V2\NuGet.Server.V2.csproj") -Configuration $Configuration -Symbols -BuildNumber $BuildNumber -Version $SemanticVersion -Branch $Branch
- New-Package (Join-Path $PSScriptRoot "src\NuGet.Server\NuGet.Server.nuspec") -Configuration $Configuration -Symbols -BuildNumber $BuildNumber -Version $SemanticVersion -Branch $Branch
+ $projects = `
+ "src\NuGet.Server.Core\NuGet.Server.Core.csproj", `
+ "src\NuGet.Server.V2\NuGet.Server.V2.csproj", `
+ "src\NuGet.Server\NuGet.Server.nuspec"
+
+ Foreach ($project in $projects) {
+ New-Package (Join-Path $PSScriptRoot $project) -Configuration $Configuration -Symbols -BuildNumber $BuildNumber -MSBuildVersion "$msBuildVersion" -Version $SemanticVersion -Branch $Branch
+ }
+ } `
+ -ev +BuildErrors
+
+Invoke-BuildStep 'Signing the packages' {
+ Sign-Packages -Configuration $Configuration -BuildNumber $BuildNumber -MSBuildVersion $msBuildVersion `
} `
-ev +BuildErrors
diff --git a/lib/NuGet-Chocolatey/NuGet.Core.dll b/lib/NuGet-Chocolatey/NuGet.Core.dll
new file mode 100644
index 0000000..9a3800e
Binary files /dev/null and b/lib/NuGet-Chocolatey/NuGet.Core.dll differ
diff --git a/samples/NuGet.Server.V2.Samples.OwinHost/DictionarySettingsProvider.cs b/samples/NuGet.Server.V2.Samples.OwinHost/DictionarySettingsProvider.cs
index 8958501..001bf47 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,19 +8,26 @@ namespace NuGet.Server.V2.Samples.OwinHost
{
public class DictionarySettingsProvider : ISettingsProvider
{
- readonly Dictionary _settings;
+ private readonly Dictionary _settings;
- public DictionarySettingsProvider(Dictionary settings)
+ public DictionarySettingsProvider(Dictionary settings)
{
_settings = 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 int GetIntSetting(string key, int defaultValue)
+ {
+ return _settings.ContainsKey(key) ? Convert.ToInt32(_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/NuGet.Server.V2.Samples.OwinHost.csproj b/samples/NuGet.Server.V2.Samples.OwinHost/NuGet.Server.V2.Samples.OwinHost.csproj
index f50c9b2..47cf0e7 100644
--- a/samples/NuGet.Server.V2.Samples.OwinHost/NuGet.Server.V2.Samples.OwinHost.csproj
+++ b/samples/NuGet.Server.V2.Samples.OwinHost/NuGet.Server.V2.Samples.OwinHost.csproj
@@ -1,5 +1,6 @@
+
Debug
@@ -9,9 +10,11 @@
Properties
NuGet.Server.V2.Samples.OwinHost
NuGet.Server.V2.Samples.OwinHost
- v4.6
+ v4.8
512
+
+
AnyCPU
@@ -38,14 +41,14 @@
NuGet.Server.V2.Samples.OwinHost.Program
-
+
False
- ..\..\packages\Microsoft.Data.Edm.5.7.0\lib\net40\Microsoft.Data.Edm.dll
+ ..\..\packages\Microsoft.Data.Edm.5.8.4\lib\net40\Microsoft.Data.Edm.dll
True
-
+
False
- ..\..\packages\Microsoft.Data.OData.5.7.0\lib\net40\Microsoft.Data.OData.dll
+ ..\..\packages\Microsoft.Data.OData.5.8.4\lib\net40\Microsoft.Data.OData.dll
True
@@ -71,10 +74,6 @@
..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll
True
-
- ..\..\packages\NuGet.Core.2.14.0\lib\net40-Client\NuGet.Core.dll
- True
-
..\..\packages\Owin.1.0\lib\net40\Owin.dll
True
@@ -87,9 +86,9 @@
False
..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll
-
+
False
- ..\..\packages\System.Spatial.5.7.0\lib\net40\System.Spatial.dll
+ ..\..\packages\System.Spatial.5.8.4\lib\net40\System.Spatial.dll
True
@@ -136,5 +135,20 @@
-
+
+ ..\..\build
+ $(BUILD_SOURCESDIRECTORY)\build
+ $(NuGetBuildPath)
+ none
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/NuGet.Server.V2.Samples.OwinHost/Program.cs b/samples/NuGet.Server.V2.Samples.OwinHost/Program.cs
index 6f952bd..46e775f 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/samples/NuGet.Server.V2.Samples.OwinHost/app.config b/samples/NuGet.Server.V2.Samples.OwinHost/app.config
index 8496c6a..f7c05d7 100644
--- a/samples/NuGet.Server.V2.Samples.OwinHost/app.config
+++ b/samples/NuGet.Server.V2.Samples.OwinHost/app.config
@@ -1,35 +1,35 @@
-
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
diff --git a/samples/NuGet.Server.V2.Samples.OwinHost/packages.config b/samples/NuGet.Server.V2.Samples.OwinHost/packages.config
index 7a404b8..bd784dc 100644
--- a/samples/NuGet.Server.V2.Samples.OwinHost/packages.config
+++ b/samples/NuGet.Server.V2.Samples.OwinHost/packages.config
@@ -1,12 +1,13 @@
+
-
-
+
+
@@ -14,5 +15,5 @@
-
+
\ No newline at end of file
diff --git a/src/NuGet.Server.Core/Core/DuplicatePackageException.cs b/src/NuGet.Server.Core/Core/DuplicatePackageException.cs
new file mode 100644
index 0000000..b37958e
--- /dev/null
+++ b/src/NuGet.Server.Core/Core/DuplicatePackageException.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NuGet.Server.Core
+{
+ ///
+ /// This exception is thrown when trying to add a package in a version that already exists on the server
+ /// and is set to false.
+ ///
+ public class DuplicatePackageException : Exception
+ {
+ public DuplicatePackageException()
+ {
+ }
+
+ public DuplicatePackageException(string message) : base(message)
+ {
+ }
+
+ public DuplicatePackageException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+
+ protected DuplicatePackageException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+ }
+}
diff --git a/src/NuGet.Server.Core/Core/NullFileSystem.cs b/src/NuGet.Server.Core/Core/NullFileSystem.cs
new file mode 100644
index 0000000..cda19b1
--- /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 0000000..59a3eb0
--- /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/DataServices/ODataPackage.cs b/src/NuGet.Server.Core/DataServices/ODataPackage.cs
index 9f2a356..fd2a0ca 100644
--- a/src/NuGet.Server.Core/DataServices/ODataPackage.cs
+++ b/src/NuGet.Server.Core/DataServices/ODataPackage.cs
@@ -66,5 +66,40 @@ public class ODataPackage
public string MinClientVersion { get; set; }
public string Language { get; set; }
+
+ #region NuSpec Enhancements
+ public string ProjectSourceUrl { get; set; }
+ public string PackageSourceUrl { get; set; }
+ public string DocsUrl { get; set; }
+ public string WikiUrl { get; set; }
+ public string MailingListUrl { get; set; }
+ public string BugTrackerUrl { get; set; }
+ public string Replaces { get; set; }
+ public string Provides { get; set; }
+ public string Conflicts { get; set; }
+ // round 2
+ public string SoftwareDisplayName { get; set; }
+ public string SoftwareDisplayVersion { get; set; }
+ #endregion
+
+ #region Server Metadata Only
+
+ public bool IsApproved { get; set; }
+ public string PackageStatus { get; set; }
+ public string PackageSubmittedStatus { get; set; }
+ public string PackageTestResultStatus { get; set; }
+ public DateTime? PackageTestResultStatusDate { get; set; }
+ public string PackageValidationResultStatus { get; set; }
+ public DateTime? PackageValidationResultDate { get; set; }
+ public DateTime? PackageCleanupResultDate { get; set; }
+ public DateTime? PackageReviewedDate { get; set; }
+ public DateTime? PackageApprovedDate { get; set; }
+ public string PackageReviewer { get; set; }
+ public bool IsDownloadCacheAvailable { get; set; }
+ public DateTime? DownloadCacheDate { get; set; }
+ public string DownloadCache { get; set; }
+
+ #endregion
+
}
}
\ No newline at end of file
diff --git a/src/NuGet.Server.Core/DataServices/PackageExtensions.cs b/src/NuGet.Server.Core/DataServices/PackageExtensions.cs
index 9f686b1..4447ccf 100644
--- a/src/NuGet.Server.Core/DataServices/PackageExtensions.cs
+++ b/src/NuGet.Server.Core/DataServices/PackageExtensions.cs
@@ -11,6 +11,8 @@ namespace NuGet.Server.Core.DataServices
{
public static class PackageExtensions
{
+ private static readonly DateTime PublishedForUnlisted = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
public static ODataPackage AsODataPackage(this IServerPackage package, ClientCompatibility compatibility)
{
return new ODataPackage
@@ -31,7 +33,7 @@ public static ODataPackage AsODataPackage(this IServerPackage package, ClientCom
Description = package.Description,
Summary = package.Summary,
ReleaseNotes = package.ReleaseNotes,
- Published = package.Created.UtcDateTime,
+ Published = package.Listed ? package.Created.UtcDateTime : PublishedForUnlisted,
LastUpdated = package.LastUpdated.UtcDateTime,
Dependencies = string.Join("|", package.DependencySets.SelectMany(ConvertDependencySetToStrings)),
PackageHash = package.PackageHash,
@@ -44,8 +46,36 @@ public static ODataPackage AsODataPackage(this IServerPackage package, ClientCom
Listed = package.Listed,
VersionDownloadCount = package.DownloadCount,
MinClientVersion = package.MinClientVersion == null ? null : package.MinClientVersion.ToString(),
- Language = package.Language
- };
+ Language = package.Language,
+ // enhancements
+ ProjectSourceUrl = package.ProjectSourceUrl == null ? null : package.ProjectSourceUrl.GetComponents(UriComponents.HttpRequestUrl, UriFormat.Unescaped),
+ PackageSourceUrl = package.PackageSourceUrl == null ? null : package.PackageSourceUrl.GetComponents(UriComponents.HttpRequestUrl, UriFormat.Unescaped),
+ DocsUrl = package.DocsUrl == null ? null : package.DocsUrl.GetComponents(UriComponents.HttpRequestUrl, UriFormat.Unescaped),
+ WikiUrl = package.WikiUrl == null ? null : package.WikiUrl.GetComponents(UriComponents.HttpRequestUrl, UriFormat.Unescaped),
+ MailingListUrl = package.MailingListUrl == null ? null : package.MailingListUrl.GetComponents(UriComponents.HttpRequestUrl, UriFormat.Unescaped),
+ BugTrackerUrl = package.BugTrackerUrl == null ? null : package.BugTrackerUrl.GetComponents(UriComponents.HttpRequestUrl, UriFormat.Unescaped),
+ Replaces = package.Replaces == null ? null : string.Join(",", package.Replaces),
+ Provides = package.Provides == null ? null : string.Join(",", package.Provides),
+ Conflicts = package.Conflicts == null ? null : string.Join(",", package.Conflicts),
+ // server metadata
+ IsApproved = package.IsApproved,
+ PackageStatus = package.PackageStatus,
+ PackageSubmittedStatus = package.PackageSubmittedStatus,
+ PackageTestResultStatus = package.PackageTestResultStatus,
+ PackageTestResultStatusDate = package.PackageTestResultStatusDate,
+ PackageValidationResultStatus = package.PackageValidationResultStatus,
+ PackageValidationResultDate = package.PackageValidationResultDate,
+ PackageCleanupResultDate = package.PackageCleanupResultDate,
+ PackageReviewedDate = package.PackageReviewedDate,
+ PackageApprovedDate = package.PackageApprovedDate,
+ PackageReviewer = package.PackageReviewer,
+ IsDownloadCacheAvailable = package.IsDownloadCacheAvailable,
+ DownloadCacheDate = package.DownloadCacheDate,
+ DownloadCache = package.DownloadCache == null ? null : string.Join("|", package.DownloadCache.Select(ConvertDownloadCacheToStrings)),
+ // enhancements round 2
+ SoftwareDisplayName = package.SoftwareDisplayName,
+ SoftwareDisplayVersion = package.SoftwareDisplayVersion,
+ };
}
private static IEnumerable ConvertDependencySetToStrings(PackageDependencySet dependencySet)
@@ -85,5 +115,10 @@ private static string ConvertDependency(PackageDependency packageDependency, Fra
return string.Format("{0}:{1}:{2}", packageDependency.Id, packageDependency.VersionSpec, VersionUtility.GetShortFrameworkName(targetFramework));
}
}
+
+ private static string ConvertDownloadCacheToStrings(DownloadCache cache)
+ {
+ return string.Format("{0}^{1}^{2}", cache.OriginalUrl, cache.FileName, cache.Checksum);
+ }
}
}
\ No newline at end of file
diff --git a/src/NuGet.Server.Core/Infrastructure/DefaultSettingsProvider.cs b/src/NuGet.Server.Core/Infrastructure/DefaultSettingsProvider.cs
index 65f31e1..b8056ba 100644
--- a/src/NuGet.Server.Core/Infrastructure/DefaultSettingsProvider.cs
+++ b/src/NuGet.Server.Core/Infrastructure/DefaultSettingsProvider.cs
@@ -8,5 +8,15 @@ public bool GetBoolSetting(string key, bool defaultValue)
{
return defaultValue;
}
+
+ public int GetIntSetting(string key, int defaultValue)
+ {
+ return defaultValue;
+ }
+
+ public string GetStringSetting(string key, string defaultValue)
+ {
+ return defaultValue;
+ }
}
}
diff --git a/src/NuGet.Server.Core/Infrastructure/IChocolateyServerPackage.cs b/src/NuGet.Server.Core/Infrastructure/IChocolateyServerPackage.cs
new file mode 100644
index 0000000..bf83ad3
--- /dev/null
+++ b/src/NuGet.Server.Core/Infrastructure/IChocolateyServerPackage.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.
+namespace NuGet.Server.Core.Infrastructure
+{
+ using System;
+ using System.Collections.Generic;
+
+ public partial interface IServerPackage
+ {
+ Uri ProjectSourceUrl { get; }
+ Uri PackageSourceUrl { get; }
+ Uri DocsUrl { get; }
+ Uri WikiUrl { get; }
+ Uri MailingListUrl { get; }
+ Uri BugTrackerUrl { get; }
+ IEnumerable Replaces { get; }
+ IEnumerable Provides { get; }
+ IEnumerable Conflicts { get; }
+
+ string SoftwareDisplayName { get; }
+ string SoftwareDisplayVersion { get; }
+
+ bool IsApproved { get; }
+ string PackageStatus { get; }
+ string PackageSubmittedStatus { get; }
+ string PackageTestResultStatus { get; }
+ DateTime? PackageTestResultStatusDate { get; }
+ string PackageValidationResultStatus { get; }
+ DateTime? PackageValidationResultDate { get; }
+ DateTime? PackageCleanupResultDate { get; }
+ DateTime? PackageReviewedDate { get; }
+ DateTime? PackageApprovedDate { get; }
+ string PackageReviewer { get; }
+
+ bool IsDownloadCacheAvailable { get; }
+ DateTime? DownloadCacheDate { get; }
+ IEnumerable DownloadCache { get; }
+
+ }
+}
diff --git a/src/NuGet.Server.Core/Infrastructure/IServerPackage.cs b/src/NuGet.Server.Core/Infrastructure/IServerPackage.cs
index bfe6460..e04584e 100644
--- a/src/NuGet.Server.Core/Infrastructure/IServerPackage.cs
+++ b/src/NuGet.Server.Core/Infrastructure/IServerPackage.cs
@@ -7,7 +7,7 @@
namespace NuGet.Server.Core.Infrastructure
{
- public interface IServerPackage
+ public partial interface IServerPackage
{
string Id { get; }
SemanticVersion Version { get; }
diff --git a/src/NuGet.Server.Core/Infrastructure/IServerPackageCache.cs b/src/NuGet.Server.Core/Infrastructure/IServerPackageCache.cs
index 6af757b..50c8d53 100644
--- a/src/NuGet.Server.Core/Infrastructure/IServerPackageCache.cs
+++ b/src/NuGet.Server.Core/Infrastructure/IServerPackageCache.cs
@@ -18,9 +18,9 @@ public interface IServerPackageCache
IEnumerable GetAll();
- void Add(ServerPackage entity);
+ void Add(ServerPackage entity, bool enableDelisting);
- void AddRange(IEnumerable entities);
+ void AddRange(IEnumerable entities, bool enableDelisting);
void Remove(string id, SemanticVersion version, bool enableDelisting);
diff --git a/src/NuGet.Server.Core/Infrastructure/IServerPackageRepository.cs b/src/NuGet.Server.Core/Infrastructure/IServerPackageRepository.cs
index b584a04..9c0c4ce 100644
--- a/src/NuGet.Server.Core/Infrastructure/IServerPackageRepository.cs
+++ b/src/NuGet.Server.Core/Infrastructure/IServerPackageRepository.cs
@@ -22,6 +22,14 @@ Task> SearchAsync(
ClientCompatibility compatibility,
CancellationToken token);
+ Task> SearchAsync(
+ string searchTerm,
+ IEnumerable targetFrameworks,
+ bool allowPrereleaseVersions,
+ bool allowUnlistedVersions,
+ ClientCompatibility compatibility,
+ CancellationToken token);
+
Task ClearCacheAsync(CancellationToken token);
Task RemovePackageAsync(string packageId, SemanticVersion version, CancellationToken token);
diff --git a/src/NuGet.Server.Core/Infrastructure/ISettingsProvider.cs b/src/NuGet.Server.Core/Infrastructure/ISettingsProvider.cs
index bd20aa5..49e56c5 100644
--- a/src/NuGet.Server.Core/Infrastructure/ISettingsProvider.cs
+++ b/src/NuGet.Server.Core/Infrastructure/ISettingsProvider.cs
@@ -5,5 +5,7 @@ namespace NuGet.Server.Core.Infrastructure
public interface ISettingsProvider
{
bool GetBoolSetting(string key, bool defaultValue);
+ string GetStringSetting(string key, string defaultValue);
+ int GetIntSetting(string key, int defaultValue);
}
}
diff --git a/src/NuGet.Server.Core/Infrastructure/JsonNetPackagesSerializer.cs b/src/NuGet.Server.Core/Infrastructure/JsonNetPackagesSerializer.cs
index cf82826..487b3ac 100644
--- a/src/NuGet.Server.Core/Infrastructure/JsonNetPackagesSerializer.cs
+++ b/src/NuGet.Server.Core/Infrastructure/JsonNetPackagesSerializer.cs
@@ -1,6 +1,7 @@
// 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;
using System.Linq;
@@ -18,7 +19,8 @@ public class JsonNetPackagesSerializer
private readonly JsonSerializer _serializer = new JsonSerializer
{
Formatting = Formatting.None,
- NullValueHandling = NullValueHandling.Ignore
+ NullValueHandling = NullValueHandling.Ignore,
+ Converters = { new AbsoluteUriConverter() },
};
public void Serialize(IEnumerable packages, Stream stream)
@@ -51,5 +53,54 @@ public IEnumerable Deserialize(Stream stream)
return packages.Packages;
}
}
+
+ ///
+ /// This is necessary because Newtonsoft.Json creates instances with
+ /// which treats UNC paths as relative. NuGet.Core uses
+ /// which treats UNC paths as absolute. For more details, see:
+ /// https://github.com/JamesNK/Newtonsoft.Json/issues/2128
+ ///
+ private class AbsoluteUriConverter : JsonConverter
+ {
+ public override bool CanConvert(Type objectType)
+ {
+ return objectType == typeof(Uri);
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ if (reader.TokenType == JsonToken.Null)
+ {
+ return null;
+ }
+ else if (reader.TokenType == JsonToken.String)
+ {
+ return new Uri((string)reader.Value, UriKind.Absolute);
+ }
+
+ throw new JsonSerializationException("The JSON value must be a string.");
+ }
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ if (value == null)
+ {
+ writer.WriteNull();
+ return;
+ }
+
+ if (!(value is Uri uriValue))
+ {
+ throw new JsonSerializationException("The value must be a URI.");
+ }
+
+ if (!uriValue.IsAbsoluteUri)
+ {
+ throw new JsonSerializationException("The URI value must be an absolute Uri. Relative URI instances are not allowed.");
+ }
+
+ writer.WriteValue(uriValue.OriginalString);
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/NuGet.Server.Core/Infrastructure/KnownPathUtility.cs b/src/NuGet.Server.Core/Infrastructure/KnownPathUtility.cs
new file mode 100644
index 0000000..eabd5c6
--- /dev/null
+++ b/src/NuGet.Server.Core/Infrastructure/KnownPathUtility.cs
@@ -0,0 +1,65 @@
+// 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.IO;
+
+namespace NuGet.Server.Core.Infrastructure
+{
+ public static class KnownPathUtility
+ {
+ ///
+ /// Determines if a relative file path could have been generated by .
+ /// The path is assumed to be relative to the base of the package directory.
+ ///
+ /// The file path to parse.
+ /// The package ID found.
+ /// The package version found.
+ /// True if the file name is known.
+ public static bool TryParseFileName(string path, out string id, out SemanticVersion version)
+ {
+ id = null;
+ version = null;
+
+ if (path == null || Path.IsPathRooted(path))
+ {
+ return false;
+ }
+
+ var pathPieces = path.Split(Path.DirectorySeparatorChar);
+ if (pathPieces.Length != 3) // {id}\{version}\{file name}
+ {
+ return false;
+ }
+
+ id = pathPieces[pathPieces.Length - 3];
+ var unparsedVersion = pathPieces[pathPieces.Length - 2];
+ var fileName = pathPieces[pathPieces.Length - 1];
+
+ if (!SemanticVersion.TryParse(unparsedVersion, out version)
+ || version.ToNormalizedString() != unparsedVersion)
+ {
+ return false;
+ }
+
+ string expectedFileName;
+ if (fileName.EndsWith(NuGet.Constants.PackageExtension))
+ {
+ expectedFileName = $"{id}.{version}{NuGet.Constants.PackageExtension}";
+ }
+ else if (fileName.EndsWith(NuGet.Constants.HashFileExtension))
+ {
+ expectedFileName = $"{id}.{version}{NuGet.Constants.HashFileExtension}";
+ }
+ else if (fileName.EndsWith(NuGet.Constants.ManifestExtension))
+ {
+ expectedFileName = $"{id}{NuGet.Constants.ManifestExtension}";
+ }
+ else
+ {
+ return false;
+ }
+
+ return expectedFileName == fileName;
+ }
+ }
+}
diff --git a/src/NuGet.Server.Core/Infrastructure/ServerPackage.cs b/src/NuGet.Server.Core/Infrastructure/ServerPackage.cs
index e5d25c3..9d1f30d 100644
--- a/src/NuGet.Server.Core/Infrastructure/ServerPackage.cs
+++ b/src/NuGet.Server.Core/Infrastructure/ServerPackage.cs
@@ -60,7 +60,41 @@ public ServerPackage(
SemVer1IsAbsoluteLatest = false;
SemVer1IsLatest = false;
SemVer2IsAbsoluteLatest = false;
- SemVer2IsLatest = false;
+ SemVer2IsLatest = false;
+
+ //enhancements
+ ProjectSourceUrl = package.ProjectSourceUrl;
+ PackageSourceUrl = package.PackageSourceUrl;
+ DocsUrl = package.DocsUrl;
+ WikiUrl = package.WikiUrl;
+ MailingListUrl = package.MailingListUrl;
+ BugTrackerUrl = package.BugTrackerUrl;
+
+
+ Replaces = package.Replaces;
+ Provides = package.Provides;
+ Conflicts = package.Conflicts;
+
+ // server metadata
+ //todo: need a place to save this local to the package itself
+ IsApproved = package.IsApproved;
+ PackageStatus = package.PackageStatus;
+ PackageSubmittedStatus = package.PackageSubmittedStatus;
+ PackageTestResultStatus = package.PackageTestResultStatus;
+ PackageTestResultStatusDate = package.PackageTestResultStatusDate;
+ PackageValidationResultStatus = package.PackageValidationResultStatus;
+ PackageValidationResultDate = package.PackageValidationResultDate;
+ PackageCleanupResultDate = package.PackageCleanupResultDate;
+ PackageReviewedDate = package.PackageReviewedDate;
+ PackageApprovedDate = package.PackageApprovedDate;
+ PackageReviewer = package.PackageReviewer;
+ IsDownloadCacheAvailable = package.IsDownloadCacheAvailable;
+ DownloadCacheDate = package.DownloadCacheDate;
+ DownloadCache = package.DownloadCache;
+
+ SoftwareDisplayName = package.SoftwareDisplayName;
+ SoftwareDisplayVersion = package.SoftwareDisplayVersion;
+
}
[JsonRequired]
@@ -97,8 +131,43 @@ public ServerPackage(
public string Copyright { get; set; }
- public string Dependencies { get; set; }
+ #region NuSpec Enhancements
+ public Uri ProjectSourceUrl { get; set; }
+ public Uri PackageSourceUrl { get; set; }
+ public Uri DocsUrl { get; set; }
+ public Uri WikiUrl { get; set; }
+ public Uri MailingListUrl { get; set; }
+ public Uri BugTrackerUrl { get; set; }
+ public IEnumerable Replaces { get; set; }
+ public IEnumerable Provides { get; set; }
+ public IEnumerable Conflicts { get; set; }
+ // round 2
+ public string SoftwareDisplayName { get; set; }
+ public string SoftwareDisplayVersion { get; set; }
+ #endregion
+
+ #region Server Metadata Only
+
+ public bool IsApproved { get; set; }
+ public string PackageStatus { get; set; }
+ public string PackageSubmittedStatus { get; set; }
+ public string PackageTestResultStatus { get; set; }
+ public DateTime? PackageTestResultStatusDate { get; set; }
+ public string PackageValidationResultStatus { get; set; }
+ public DateTime? PackageValidationResultDate { get; set; }
+ public DateTime? PackageCleanupResultDate { get; set; }
+ public DateTime? PackageReviewedDate { get; set; }
+ public DateTime? PackageApprovedDate { get; set; }
+ public string PackageReviewer { get; set; }
+ public bool IsDownloadCacheAvailable { get; set; }
+ public DateTime? DownloadCacheDate { get; set; }
+ public IEnumerable DownloadCache { get; set; }
+
+ #endregion
+
+ public string Dependencies { get; set; }
+
private List _dependencySets;
[JsonIgnore]
diff --git a/src/NuGet.Server.Core/Infrastructure/ServerPackageCache.cs b/src/NuGet.Server.Core/Infrastructure/ServerPackageCache.cs
index 5e9f414..7092c09 100644
--- a/src/NuGet.Server.Core/Infrastructure/ServerPackageCache.cs
+++ b/src/NuGet.Server.Core/Infrastructure/ServerPackageCache.cs
@@ -130,7 +130,7 @@ public void Remove(string id, SemanticVersion version, bool enableDelisting)
_packages.RemoveWhere(p => IsMatch(p, id, version));
}
- UpdateLatestVersions(_packages.Where(p => IsMatch(p, id)));
+ UpdateLatestVersions(_packages.Where(p => IsMatch(p, id)), enableDelisting);
_isDirty = true;
}
@@ -140,7 +140,7 @@ public void Remove(string id, SemanticVersion version, bool enableDelisting)
}
}
- public void Add(ServerPackage entity)
+ public void Add(ServerPackage entity, bool enableDelisting)
{
_syncLock.EnterWriteLock();
try
@@ -148,7 +148,7 @@ public void Add(ServerPackage entity)
_packages.Remove(entity);
_packages.Add(entity);
- UpdateLatestVersions(_packages.Where(p => IsMatch(p, entity.Id)));
+ UpdateLatestVersions(_packages.Where(p => IsMatch(p, entity.Id)), enableDelisting);
_isDirty = true;
}
@@ -158,7 +158,7 @@ public void Add(ServerPackage entity)
}
}
- public void AddRange(IEnumerable entities)
+ public void AddRange(IEnumerable entities, bool enableDelisting)
{
_syncLock.EnterWriteLock();
try
@@ -169,7 +169,7 @@ public void AddRange(IEnumerable entities)
_packages.Add(entity);
}
- UpdateLatestVersions(_packages);
+ UpdateLatestVersions(_packages, enableDelisting);
_isDirty = true;
}
@@ -179,7 +179,7 @@ public void AddRange(IEnumerable entities)
}
}
- private static void UpdateLatestVersions(IEnumerable packages)
+ private static void UpdateLatestVersions(IEnumerable packages, bool enableDelisting)
{
var semVer1AbsoluteLatest = InitializePackageDictionary();
var semVer1Latest = InitializePackageDictionary();
@@ -195,6 +195,12 @@ private static void UpdateLatestVersions(IEnumerable packages)
package.SemVer2IsAbsoluteLatest = false;
package.SemVer2IsLatest = false;
+ // Unlisted packages are never considered "latest".
+ if (enableDelisting && !package.Listed)
+ {
+ return;
+ }
+
// Update the SemVer1 views.
if (!package.IsSemVer2)
{
diff --git a/src/NuGet.Server.Core/Infrastructure/ServerPackageRepository.cs b/src/NuGet.Server.Core/Infrastructure/ServerPackageRepository.cs
index 04f936d..4b8a886 100644
--- a/src/NuGet.Server.Core/Infrastructure/ServerPackageRepository.cs
+++ b/src/NuGet.Server.Core/Infrastructure/ServerPackageRepository.cs
@@ -32,6 +32,7 @@ public class ServerPackageRepository
private readonly bool _runBackgroundTasks;
private FileSystemWatcher _fileSystemWatcher;
+ private string _watchDirectory;
private bool _isFileSystemWatcherSuppressed;
private bool _needsRebuild;
@@ -58,7 +59,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),
@@ -72,21 +73,16 @@ internal ServerPackageRepository(
ISettingsProvider settingsProvider = null,
Logging.ILogger logger = null)
{
- if (fileSystem == null)
- {
- throw new ArgumentNullException(nameof(fileSystem));
- }
-
if (innerRepository == null)
{
throw new ArgumentNullException(nameof(innerRepository));
}
- _fileSystem = fileSystem;
+ _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
_runBackgroundTasks = runBackgroundTasks;
_settingsProvider = settingsProvider ?? new DefaultSettingsProvider();
_logger = logger ?? new TraceLogger();
- _serverPackageCache = InitializeServerPackageStore();
+ _serverPackageCache = InitializeServerPackageCache();
_serverPackageStore = new ServerPackageStore(
_fileSystem,
innerRepository,
@@ -110,9 +106,68 @@ internal ServerPackageRepository(
private bool EnableFileSystemMonitoring =>
_settingsProvider.GetBoolSetting("enableFileSystemMonitoring", true);
- private ServerPackageCache InitializeServerPackageStore()
+ private string CacheFileName => _settingsProvider.GetStringSetting("cacheFileName", null);
+
+ private TimeSpan InitialCacheRebuildAfter
+ {
+ get
+ {
+ var value = GetPositiveIntSetting("initialCacheRebuildAfterSeconds", 15);
+ return TimeSpan.FromSeconds(value);
+ }
+ }
+
+ private TimeSpan CacheRebuildFrequency
{
- return new ServerPackageCache(_fileSystem, Environment.MachineName.ToLowerInvariant() + ".cache.bin");
+ get
+ {
+ int value = GetPositiveIntSetting("cacheRebuildFrequencyInMinutes", 60);
+ return TimeSpan.FromMinutes(value);
+ }
+ }
+
+ private int GetPositiveIntSetting(string name, int defaultValue)
+ {
+ var value = _settingsProvider.GetIntSetting(name, defaultValue);
+ if (value <= 0)
+ {
+ value = defaultValue;
+ }
+
+ return value;
+ }
+
+ private ServerPackageCache InitializeServerPackageCache()
+ {
+ 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;
}
///
@@ -188,6 +243,17 @@ public async Task> SearchAsync(
bool allowPrereleaseVersions,
ClientCompatibility compatibility,
CancellationToken token)
+ {
+ return await SearchAsync(searchTerm, targetFrameworks, allowPrereleaseVersions, false, compatibility, token);
+ }
+
+ public async Task> SearchAsync(
+ string searchTerm,
+ IEnumerable targetFrameworks,
+ bool allowPrereleaseVersions,
+ bool allowUnlistedVersions,
+ ClientCompatibility compatibility,
+ CancellationToken token)
{
var cache = await GetPackagesAsync(compatibility, token);
@@ -195,7 +261,7 @@ public async Task> SearchAsync(
.Find(searchTerm)
.FilterByPrerelease(allowPrereleaseVersions);
- if (EnableDelisting)
+ if (EnableDelisting && !allowUnlistedVersions)
{
packages = packages.Where(p => p.Listed);
}
@@ -241,7 +307,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))
{
@@ -272,7 +338,7 @@ private void AddPackagesFromDropFolderWithoutLocking()
}
// Add packages to metadata store in bulk
- _serverPackageCache.AddRange(serverPackages);
+ _serverPackageCache.AddRange(serverPackages, EnableDelisting);
_serverPackageCache.PersistIfDirty();
_logger.Log(LogLevel.Info, "Finished adding packages from drop folder.");
@@ -302,7 +368,7 @@ public async Task AddPackageAsync(IPackage package, CancellationToken token)
EnableDelisting);
// Add the package to the metadata store.
- _serverPackageCache.Add(serverPackage);
+ _serverPackageCache.Add(serverPackage, EnableDelisting);
_logger.Log(LogLevel.Info, "Finished adding package {0} {1}.", package.Id, package.Version);
}
@@ -334,7 +400,7 @@ private bool CanPackageBeAddedWithoutLocking(IPackage package, bool shouldThrow)
if (shouldThrow)
{
- throw new InvalidOperationException(message);
+ throw new DuplicatePackageException(message);
}
return false;
@@ -428,7 +494,7 @@ private async Task RebuildPackageStoreWithoutLockingAsync(CancellationToken toke
// Build cache
var packages = await ReadPackagesFromDiskWithoutLockingAsync(token);
_serverPackageCache.Clear();
- _serverPackageCache.AddRange(packages);
+ _serverPackageCache.AddRange(packages, EnableDelisting);
// Add packages from drop folder
AddPackagesFromDropFolderWithoutLocking();
@@ -470,7 +536,7 @@ private async Task> ReadPackagesFromDiskWithoutLockingAsy
throw;
}
}
-
+
///
/// Sets the current cache to null so it will be regenerated next time.
///
@@ -492,18 +558,21 @@ private void SetupBackgroundJobs()
_logger.Log(LogLevel.Info, "Registering background jobs...");
// Persist to package store at given interval (when dirty)
+ _logger.Log(LogLevel.Info, "Persisting the cache file every 1 minute.");
_persistenceTimer = new Timer(
callback: state => _serverPackageCache.PersistIfDirty(),
state: null,
dueTime: TimeSpan.FromMinutes(1),
period: TimeSpan.FromMinutes(1));
- // Rebuild the package store in the background (every hour)
+ // Rebuild the package store in the background
+ _logger.Log(LogLevel.Info, "Rebuilding the cache file for the first time after {0} second(s).", InitialCacheRebuildAfter.TotalSeconds);
+ _logger.Log(LogLevel.Info, "Rebuilding the cache file every {0} hour(s).", CacheRebuildFrequency.TotalHours);
_rebuildTimer = new Timer(
callback: state => RebuildPackageStoreAsync(CancellationToken.None),
state: null,
- dueTime: TimeSpan.FromSeconds(15),
- period: TimeSpan.FromHours(1));
+ dueTime: InitialCacheRebuildAfter,
+ period: CacheRebuildFrequency);
_logger.Log(LogLevel.Info, "Finished registering background jobs.");
}
@@ -517,9 +586,14 @@ private void RegisterFileSystemWatcher()
if (EnableFileSystemMonitoring && _runBackgroundTasks && _fileSystemWatcher == null && !string.IsNullOrEmpty(Source) && Directory.Exists(Source))
{
// ReSharper disable once UseObjectOrCollectionInitializer
- _fileSystemWatcher = new FileSystemWatcher(Source);
- _fileSystemWatcher.Filter = "*";
- _fileSystemWatcher.IncludeSubdirectories = true;
+ _fileSystemWatcher = new FileSystemWatcher(Source)
+ {
+ Filter = "*",
+ IncludeSubdirectories = true,
+ };
+
+ //Keep the normalized watch path.
+ _watchDirectory = Path.GetFullPath(_fileSystemWatcher.Path);
_fileSystemWatcher.Changed += FileSystemChangedAsync;
_fileSystemWatcher.Created += FileSystemChangedAsync;
@@ -549,8 +623,9 @@ private void UnregisterFileSystemWatcher()
_logger.Log(LogLevel.Verbose, "Destroyed FileSystemWatcher - no longer monitoring {0}.", Source);
}
- }
+ _watchDirectory = null;
+ }
///
/// This is an event handler for background work. Therefore, it should never throw exceptions.
@@ -564,10 +639,24 @@ private async void FileSystemChangedAsync(object sender, FileSystemEventArgs e)
return;
}
+ if (ShouldIgnoreFileSystemEvent(e))
+ {
+ _logger.Log(LogLevel.Verbose, "File system event ignored. File: {0} - Change: {1}", e.Name, e.ChangeType);
+ return;
+ }
+
_logger.Log(LogLevel.Verbose, "File system changed. File: {0} - Change: {1}", e.Name, e.ChangeType);
+ var changedDirectory = Path.GetDirectoryName(e.FullPath);
+ if (changedDirectory == null || _watchDirectory == null)
+ {
+ return;
+ }
+
+ changedDirectory = Path.GetFullPath(changedDirectory);
+
// 1) If a .nupkg is dropped in the root, add it as a package
- if (string.Equals(Path.GetDirectoryName(e.FullPath), _fileSystemWatcher.Path, StringComparison.OrdinalIgnoreCase)
+ if (string.Equals(changedDirectory, _watchDirectory, StringComparison.OrdinalIgnoreCase)
&& string.Equals(Path.GetExtension(e.Name), ".nupkg", StringComparison.OrdinalIgnoreCase))
{
// When a package is dropped into the server packages root folder, add it to the repository.
@@ -575,7 +664,7 @@ private async void FileSystemChangedAsync(object sender, FileSystemEventArgs e)
}
// 2) If a file is updated in a subdirectory, *or* a folder is deleted, invalidate the cache
- if ((!string.Equals(Path.GetDirectoryName(e.FullPath), _fileSystemWatcher.Path, StringComparison.OrdinalIgnoreCase) && File.Exists(e.FullPath))
+ if ((!string.Equals(changedDirectory, _watchDirectory, StringComparison.OrdinalIgnoreCase) && File.Exists(e.FullPath))
|| e.ChangeType == WatcherChangeTypes.Deleted)
{
// TODO: invalidating *all* packages for every nupkg change under this folder seems more expensive than it should.
@@ -590,6 +679,59 @@ private async void FileSystemChangedAsync(object sender, FileSystemEventArgs e)
}
}
+ private bool ShouldIgnoreFileSystemEvent(FileSystemEventArgs e)
+ {
+ // We can only ignore Created or Changed events. All other types are always processed. Eventually we could
+ // try to ignore some Deleted events in the case of API package delete, but this is harder.
+ if (e.ChangeType != WatcherChangeTypes.Created
+ && e.ChangeType != WatcherChangeTypes.Changed)
+ {
+ _logger.Log(LogLevel.Verbose, "The file system event change type is not ignorable.");
+ return false;
+ }
+
+ /// We can only ignore events related to file paths changed by the
+ /// . If the file system event is representing a known file path
+ /// extracted during package push, we can ignore the event. File system events are supressed during package
+ /// push but this is still necessary since file system events can come some time after the suppression
+ /// window has ended.
+ if (!KnownPathUtility.TryParseFileName(e.Name, out var id, out var version))
+ {
+ _logger.Log(LogLevel.Verbose, "The file system event is not related to a known package path.");
+ return false;
+ }
+
+ /// The file path could have been generated by . Now
+ /// determine if the package is in the cache.
+ var matchingPackage = _serverPackageCache
+ .GetAll()
+ .Where(p => StringComparer.OrdinalIgnoreCase.Equals(p.Id, id))
+ .Where(p => version.Equals(p.Version))
+ .FirstOrDefault();
+
+ if (matchingPackage == null)
+ {
+ _logger.Log(LogLevel.Verbose, "The file system event is not related to a known package.");
+ return false;
+ }
+
+ var fileInfo = new FileInfo(e.FullPath);
+ if (!fileInfo.Exists)
+ {
+ _logger.Log(LogLevel.Verbose, "The package file is missing.");
+ return false;
+ }
+
+ var minimumCreationTime = DateTimeOffset.UtcNow.AddMinutes(-1);
+ if (fileInfo.CreationTimeUtc < minimumCreationTime)
+ {
+ _logger.Log(LogLevel.Verbose, "The package file was not created recently.");
+ return false;
+ }
+
+ return true;
+ }
+
private async Task LockAsync(CancellationToken token)
{
var handle = new Lock(_syncLock);
@@ -643,12 +785,7 @@ private sealed class SuppressedFileSystemWatcher : IDisposable
public SuppressedFileSystemWatcher(ServerPackageRepository repository)
{
- if (repository == null)
- {
- throw new ArgumentNullException(nameof(repository));
- }
-
- _repository = repository;
+ _repository = repository ?? throw new ArgumentNullException(nameof(repository));
}
public bool LockTaken => _lockHandle.LockTaken;
@@ -663,8 +800,8 @@ public void Dispose()
{
if (_lockHandle != null && _lockHandle.LockTaken)
{
- _lockHandle.Dispose();
_repository._isFileSystemWatcherSuppressed = false;
+ _lockHandle.Dispose();
}
}
}
diff --git a/src/NuGet.Server.Core/Infrastructure/ServerPackageRepositoryExtensions.cs b/src/NuGet.Server.Core/Infrastructure/ServerPackageRepositoryExtensions.cs
index 2d9acf2..e5d782f 100644
--- a/src/NuGet.Server.Core/Infrastructure/ServerPackageRepositoryExtensions.cs
+++ b/src/NuGet.Server.Core/Infrastructure/ServerPackageRepositoryExtensions.cs
@@ -38,6 +38,23 @@ public static async Task> SearchAsync(
token);
}
+ public static async Task> SearchAsync(
+ this IServerPackageRepository repository,
+ string searchTerm,
+ bool allowPrereleaseVersions,
+ bool allowUnlistedVersions,
+ ClientCompatibility compatibility,
+ CancellationToken token)
+ {
+ return await repository.SearchAsync(
+ searchTerm,
+ Enumerable.Empty(),
+ allowPrereleaseVersions,
+ allowUnlistedVersions,
+ compatibility,
+ token);
+ }
+
public static async Task FindPackageAsync(
this IServerPackageRepository repository,
string id,
diff --git a/src/NuGet.Server.Core/Infrastructure/ServerPackageStore.cs b/src/NuGet.Server.Core/Infrastructure/ServerPackageStore.cs
index 94eb648..533c850 100644
--- a/src/NuGet.Server.Core/Infrastructure/ServerPackageStore.cs
+++ b/src/NuGet.Server.Core/Infrastructure/ServerPackageStore.cs
@@ -45,9 +45,7 @@ public void Remove(string id, SemanticVersion version, bool enableDelisting)
{
if (enableDelisting)
{
- var physicalFileSystem = _fileSystem as PhysicalFileSystem;
-
- if (physicalFileSystem != null)
+ if (_fileSystem is PhysicalFileSystem physicalFileSystem)
{
var fileName = physicalFileSystem.GetFullPath(
GetPackageFileName(id, version.ToNormalizedString()));
@@ -150,10 +148,7 @@ private PackageDerivedData GetPackageDerivedData(IPackage package, bool enableDe
var normalizedVersion = package.Version.ToNormalizedString();
var packageFileName = GetPackageFileName(package.Id, normalizedVersion);
var hashFileName = GetHashFileName(package.Id, normalizedVersion);
-
- // File system
- var physicalFileSystem = _fileSystem as PhysicalFileSystem;
-
+
// Build package info
var packageDerivedData = new PackageDerivedData();
@@ -165,7 +160,7 @@ private PackageDerivedData GetPackageDerivedData(IPackage package, bool enableDe
// Read package info
var localPackage = package as LocalPackage;
- if (physicalFileSystem != null)
+ if (_fileSystem is PhysicalFileSystem physicalFileSystem)
{
// Read package info from file system
var fullPath = _fileSystem.GetFullPath(packageFileName);
diff --git a/src/NuGet.Server.Core/NuGet.Server.Core.csproj b/src/NuGet.Server.Core/NuGet.Server.Core.csproj
index 07c3a26..6b51387 100644
--- a/src/NuGet.Server.Core/NuGet.Server.Core.csproj
+++ b/src/NuGet.Server.Core/NuGet.Server.Core.csproj
@@ -1,5 +1,6 @@
-
+
+
Debug
@@ -9,9 +10,11 @@
Properties
NuGet.Server.Core
NuGet.Server.Core
- v4.6
+ v4.8
512
+
+
true
@@ -39,9 +42,9 @@
..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll
True
-
- ..\..\packages\NuGet.Core.2.14.0\lib\net40-Client\NuGet.Core.dll
- True
+
+ False
+ ..\..\lib\NuGet-Chocolatey\NuGet.Core.dll
@@ -57,8 +60,11 @@
HashCodeCombiner.cs
+
+
+
@@ -70,6 +76,8 @@
+
+
@@ -111,5 +119,20 @@
-
-
\ No newline at end of file
+
+ ..\..\build
+ $(BUILD_SOURCESDIRECTORY)\build
+ $(NuGetBuildPath)
+ none
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
+
diff --git a/src/NuGet.Server.Core/NuGet.Server.Core.nuspec b/src/NuGet.Server.Core/NuGet.Server.Core.nuspec
index e1a8ca4..4e4bf92 100644
--- a/src/NuGet.Server.Core/NuGet.Server.Core.nuspec
+++ b/src/NuGet.Server.Core/NuGet.Server.Core.nuspec
@@ -4,11 +4,12 @@
$id$
$version$
$description$
- .NET Foundation
- $author$
- https://raw.githubusercontent.com/NuGet/NuGet.Server/dev/LICENSE.txt
+ Microsoft
+ Microsoft
+ Apache-2.0
https://github.com/NuGet/NuGet.Server
- $copyright$
+ © Microsoft Corporation. All rights reserved.
+
diff --git a/src/NuGet.Server.Core/Strings.Designer.cs b/src/NuGet.Server.Core/Strings.Designer.cs
index 687e05d..7aaf25b 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 f3e2b0b..9b10159 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.Core/packages.config b/src/NuGet.Server.Core/packages.config
index 6397f52..ca36acf 100644
--- a/src/NuGet.Server.Core/packages.config
+++ b/src/NuGet.Server.Core/packages.config
@@ -1,5 +1,6 @@
+
diff --git a/src/NuGet.Server.V2/Controllers/NuGetODataController.cs b/src/NuGet.Server.V2/Controllers/NuGetODataController.cs
index 250b581..77b63ad 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;
@@ -40,20 +41,12 @@ protected NuGetODataController(
IServerPackageRepository repository,
IPackageAuthenticationService authenticationService = null)
{
- if (repository == null)
- {
- throw new ArgumentNullException(nameof(repository));
- }
-
- _serverRepository = repository;
+ _serverRepository = repository ?? throw new ArgumentNullException(nameof(repository));
_authenticationService = authenticationService;
}
-
+
// GET /Packages
- // Never seen this invoked. NuGet.Exe and Visual Studio seems to use 'Search' for all package listing.
- // Probably required to be OData compliant?
[HttpGet]
- [EnableQuery(PageSize = 100, HandleNullPropagation = HandleNullPropagationOption.False)]
public virtual async Task Get(
ODataQueryOptions options,
[FromUri] string semVerLevel = "",
@@ -136,8 +129,8 @@ public virtual IHttpActionResult GetPropertyFromPackages(string propertyName, st
[HttpPost]
public virtual async Task Search(
ODataQueryOptions options,
- [FromODataUri] string searchTerm = "",
- [FromODataUri] string targetFramework = "",
+ [FromODataUri] string searchTerm = "",
+ [FromODataUri] string targetFramework = "",
[FromODataUri] bool includePrerelease = false,
[FromODataUri] bool includeDelisted = false,
[FromUri] string semVerLevel = "",
@@ -151,6 +144,7 @@ public virtual async Task Search(
searchTerm,
targetFrameworks,
includePrerelease,
+ includeDelisted,
clientCompatibility,
token);
@@ -210,8 +204,8 @@ public virtual async Task GetUpdates(
var idValues = packageIds.Trim().Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
var versionValues = versions.Trim().Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
- var targetFrameworkValues = String.IsNullOrEmpty(targetFrameworks)
- ? null
+ var targetFrameworkValues = String.IsNullOrEmpty(targetFrameworks)
+ ? null
: targetFrameworks.Split('|').Select(VersionUtility.ParseFrameworkName).ToList();
var versionConstraintValues = (String.IsNullOrEmpty(versionConstraints)
? new string[idValues.Length]
@@ -226,8 +220,7 @@ public virtual async Task GetUpdates(
var packagesToUpdate = new List();
for (var i = 0; i < idValues.Length; i++)
{
- SemanticVersion semVersion;
- if(SemanticVersion.TryParse(versionValues[i],out semVersion))
+ if (SemanticVersion.TryParse(versionValues[i], out var semVersion))
{
packagesToUpdate.Add(new PackageBuilder { Id = idValues[i], Version = semVersion });
}
@@ -373,7 +366,7 @@ public virtual async Task DeletePackage(
}
else
{
- return CreateStringResponse(HttpStatusCode.Forbidden, string.Format("Access denied for package '{0}', version '{1}'.", requestedPackage.Id,version));
+ return CreateStringResponse(HttpStatusCode.Forbidden, string.Format("Access denied for package '{0}', version '{1}'.", requestedPackage.Id, version));
}
}
@@ -405,14 +398,20 @@ 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))
{
- await _serverRepository.AddPackageAsync(package, token);
- retValue = Request.CreateResponse(HttpStatusCode.Created);
+ try
+ {
+ await _serverRepository.AddPackageAsync(package, token);
+ retValue = Request.CreateResponse(HttpStatusCode.Created);
+ }
+ catch (DuplicatePackageException ex)
+ {
+ retValue = CreateStringResponse(HttpStatusCode.Conflict, ex.Message);
+ }
}
else
{
@@ -425,7 +424,7 @@ public virtual async Task UploadPackage(CancellationToken t
File.Delete(temporaryFile);
}
catch (Exception)
- {
+ {
retValue = CreateStringResponse(HttpStatusCode.InternalServerError, "Could not remove temporary upload file.");
}
@@ -441,12 +440,11 @@ protected HttpResponseMessage CreateStringResponse(HttpStatusCode statusCode, st
private string GetApiKeyFromHeader()
{
string apiKey = null;
- IEnumerable values;
- if (Request.Headers.TryGetValues(ApiKeyHeader, out values))
+ if (Request.Headers.TryGetValues(ApiKeyHeader, out var values))
{
apiKey = values.FirstOrDefault();
}
-
+
return apiKey;
}
diff --git a/src/NuGet.Server.V2/NuGet.Server.V2.csproj b/src/NuGet.Server.V2/NuGet.Server.V2.csproj
index 2d1f801..a051830 100644
--- a/src/NuGet.Server.V2/NuGet.Server.V2.csproj
+++ b/src/NuGet.Server.V2/NuGet.Server.V2.csproj
@@ -1,5 +1,6 @@
-
+
+
Debug
@@ -9,9 +10,11 @@
Properties
NuGet.Server.V2
NuGet.Server.V2
- v4.6
+ v4.8
512
+
+
true
@@ -33,14 +36,14 @@
false
-
+
False
- ..\..\packages\Microsoft.Data.Edm.5.7.0\lib\net40\Microsoft.Data.Edm.dll
+ ..\..\packages\Microsoft.Data.Edm.5.8.4\lib\net40\Microsoft.Data.Edm.dll
True
-
+
False
- ..\..\packages\Microsoft.Data.OData.5.7.0\lib\net40\Microsoft.Data.OData.dll
+ ..\..\packages\Microsoft.Data.OData.5.8.4\lib\net40\Microsoft.Data.OData.dll
True
@@ -51,9 +54,9 @@
..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll
True
-
- ..\..\packages\NuGet.Core.2.14.0\lib\net40-Client\NuGet.Core.dll
- True
+
+ False
+ ..\..\lib\NuGet-Chocolatey\NuGet.Core.dll
@@ -64,9 +67,9 @@
..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll
True
-
+
False
- ..\..\packages\System.Spatial.5.7.0\lib\net40\System.Spatial.dll
+ ..\..\packages\System.Spatial.5.8.4\lib\net40\System.Spatial.dll
True
@@ -117,5 +120,20 @@
-
-
\ No newline at end of file
+
+ ..\..\build
+ $(BUILD_SOURCESDIRECTORY)\build
+ $(NuGetBuildPath)
+ none
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
+
diff --git a/src/NuGet.Server.V2/NuGet.Server.V2.nuspec b/src/NuGet.Server.V2/NuGet.Server.V2.nuspec
index 82a7e1d..decea47 100644
--- a/src/NuGet.Server.V2/NuGet.Server.V2.nuspec
+++ b/src/NuGet.Server.V2/NuGet.Server.V2.nuspec
@@ -4,11 +4,12 @@
$id$
$version$
$description$
- .NET Foundation
- $author$
- https://raw.githubusercontent.com/NuGet/NuGet.Server/dev/LICENSE.txt
+ Microsoft
+ Microsoft
+ Apache-2.0
https://github.com/NuGet/NuGet.Server
- $copyright$
+ © Microsoft Corporation. All rights reserved.
+
@@ -22,9 +23,9 @@
-
-
-
+
+
+
diff --git a/src/NuGet.Server.V2/NuGetV2WebApiEnabler.cs b/src/NuGet.Server.V2/NuGetV2WebApiEnabler.cs
index 98830c9..f24a5af 100644
--- a/src/NuGet.Server.V2/NuGetV2WebApiEnabler.cs
+++ b/src/NuGet.Server.V2/NuGetV2WebApiEnabler.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.Linq;
using System.Net.Http;
@@ -19,10 +20,46 @@ namespace NuGet.Server.V2
{
public static class NuGetV2WebApiEnabler
{
+ ///
+ /// Enables the NuGet V2 protocol routes on this . Note that this method does
+ /// not activate the legacy push URL, api/v2/package
. To activate the legacy push route, use the
+ /// method overload.
+ ///
+ /// The HTTP configuration associated with your web app.
+ /// The route name prefix, to allow multiple feeds per web app.
+ /// The base URL for the routes, to allow multiple feeds per web app.
+ /// The name of the OData controller containing the actions.
+ /// The provided, for chaining purposes.
public static HttpConfiguration UseNuGetV2WebApiFeed(this HttpConfiguration config,
string routeName,
- string routeUrlRoot,
+ string routeUrlRoot,
string oDatacontrollerName)
+ {
+ return config.UseNuGetV2WebApiFeed(
+ routeName,
+ routeUrlRoot,
+ oDatacontrollerName,
+ enableLegacyPushRoute: false);
+ }
+
+ ///
+ /// Enables the NuGet V2 protocol routes on this .
+ ///
+ /// The HTTP configuration associated with your web app.
+ /// The route name prefix, to allow multiple feeds per web app.
+ /// The base URL for the routes, to allow multiple feeds per web app.
+ /// The name of the OData controller containing the actions.
+ ///
+ /// Whether or not to enable the legacy push URL, api/v2/package
. Note that this route does not
+ /// use the prefix or and therefore should only
+ /// be enabled once (i.e. on a single controller).
+ ///
+ /// The provided, for chaining purposes.
+ public static HttpConfiguration UseNuGetV2WebApiFeed(this HttpConfiguration config,
+ string routeName,
+ string routeUrlRoot,
+ string oDatacontrollerName,
+ bool enableLegacyPushRoute)
{
// Insert conventions to make NuGet-compatible OData feed possible
var conventions = ODataRoutingConventions.CreateDefault();
@@ -53,6 +90,16 @@ public static HttpConfiguration UseNuGetV2WebApiFeed(this HttpConfiguration conf
constraints: new { httpMethod = new HttpMethodConstraint(HttpMethod.Delete) }
);
+ if (enableLegacyPushRoute)
+ {
+ config.Routes.MapHttpRoute(
+ name: "apiv2package_upload",
+ routeTemplate: "api/v2/package",
+ defaults: new { controller = oDatacontrollerName, action = "UploadPackage" },
+ constraints: new { httpMethod = new HttpMethodConstraint(HttpMethod.Put) }
+ );
+ }
+
config.Routes.MapODataServiceRoute(routeName, routeUrlRoot, oDataModel, new CountODataPathHandler(), conventions);
return config;
}
diff --git a/src/NuGet.Server.V2/app.config b/src/NuGet.Server.V2/app.config
index 26bb328..42f1cdd 100644
--- a/src/NuGet.Server.V2/app.config
+++ b/src/NuGet.Server.V2/app.config
@@ -16,15 +16,15 @@
-
+
-
+
-
+
diff --git a/src/NuGet.Server.V2/packages.config b/src/NuGet.Server.V2/packages.config
index 3b1ee39..30d4e7d 100644
--- a/src/NuGet.Server.V2/packages.config
+++ b/src/NuGet.Server.V2/packages.config
@@ -1,12 +1,13 @@
+
-
-
+
+
-
+
\ No newline at end of file
diff --git a/src/NuGet.Server/App_Start/NuGetODataConfig.cs b/src/NuGet.Server/App_Start/NuGetODataConfig.cs
index 56bec91..2d1a056 100644
--- a/src/NuGet.Server/App_Start/NuGetODataConfig.cs
+++ b/src/NuGet.Server/App_Start/NuGetODataConfig.cs
@@ -3,8 +3,10 @@
using System.Net.Http;
using System.Web.Http;
+using System.Web.Http.ExceptionHandling;
using System.Web.Http.Routing;
using NuGet.Server.DataServices;
+using NuGet.Server.Infrastructure;
using NuGet.Server.V2;
// The consuming project executes this logic with its own copy of this class. This is done with a .pp file that is
@@ -26,7 +28,17 @@ public static void Start()
public static void Initialize(HttpConfiguration config, string controllerName)
{
- NuGetV2WebApiEnabler.UseNuGetV2WebApiFeed(config, "NuGetDefault", "nuget", controllerName);
+ NuGetV2WebApiEnabler.UseNuGetV2WebApiFeed(
+ config,
+ "NuGetDefault",
+ "nuget",
+ controllerName,
+ enableLegacyPushRoute: true);
+
+ config.Services.Replace(typeof(IExceptionLogger), new TraceExceptionLogger());
+
+ // Trace.Listeners.Add(new TextWriterTraceListener(HostingEnvironment.MapPath("~/NuGet.Server.log")));
+ // Trace.AutoFlush = true;
config.Routes.MapHttpRoute(
name: "NuGetDefault_ClearCache",
diff --git a/src/NuGet.Server/App_Start/NuGetODataConfig.cs.pp b/src/NuGet.Server/App_Start/NuGetODataConfig.cs.pp
index bba5618..ac6dfec 100644
--- a/src/NuGet.Server/App_Start/NuGetODataConfig.cs.pp
+++ b/src/NuGet.Server/App_Start/NuGetODataConfig.cs.pp
@@ -1,7 +1,9 @@
using System.Net.Http;
using System.Web.Http;
+using System.Web.Http.ExceptionHandling;
using System.Web.Http.Routing;
using NuGet.Server;
+using NuGet.Server.Infrastructure;
using NuGet.Server.V2;
[assembly: WebActivatorEx.PreApplicationStartMethod(typeof($rootnamespace$.App_Start.NuGetODataConfig), "Start")]
@@ -9,14 +11,24 @@
namespace $rootnamespace$.App_Start
{
public static class NuGetODataConfig
- {
+ {
public static void Start()
- {
+ {
ServiceResolver.SetServiceResolver(new DefaultServiceResolver());
var config = GlobalConfiguration.Configuration;
- NuGetV2WebApiEnabler.UseNuGetV2WebApiFeed(config, "NuGetDefault", "nuget", "PackagesOData");
+ NuGetV2WebApiEnabler.UseNuGetV2WebApiFeed(
+ config,
+ "NuGetDefault",
+ "nuget",
+ "PackagesOData",
+ enableLegacyPushRoute: true);
+
+ config.Services.Replace(typeof(IExceptionLogger), new TraceExceptionLogger());
+
+ // Trace.Listeners.Add(new TextWriterTraceListener(HostingEnvironment.MapPath("~/NuGet.Server.log")));
+ // Trace.AutoFlush = true;
config.Routes.MapHttpRoute(
name: "NuGetDefault_ClearCache",
diff --git a/src/NuGet.Server/Controllers/PackagesODataController.cs b/src/NuGet.Server/Controllers/PackagesODataController.cs
index f68cea5..0862d9f 100644
--- a/src/NuGet.Server/Controllers/PackagesODataController.cs
+++ b/src/NuGet.Server/Controllers/PackagesODataController.cs
@@ -29,7 +29,7 @@ protected PackagesODataController(IServiceResolver serviceResolver)
// Exposed through ordinary Web API route. Bypasses OData pipeline.
public async Task ClearCache(CancellationToken token)
{
- if (RequestContext.IsLocal)
+ if (RequestContext.IsLocal || ServiceResolver.Current.Resolve().GetBoolSetting("allowRemoteCacheManagement", false))
{
await _serverRepository.ClearCacheAsync(token);
return CreateStringResponse(HttpStatusCode.OK, "Server cache has been cleared.");
diff --git a/src/NuGet.Server/Core/DefaultServiceResolver.cs b/src/NuGet.Server/Core/DefaultServiceResolver.cs
index dae8fab..af627b8 100644
--- a/src/NuGet.Server/Core/DefaultServiceResolver.cs
+++ b/src/NuGet.Server/Core/DefaultServiceResolver.cs
@@ -13,6 +13,7 @@ namespace NuGet.Server
public sealed class DefaultServiceResolver
: IServiceResolver, IDisposable
{
+ private readonly Core.Logging.ILogger _logger;
private readonly CryptoHashProvider _hashProvider;
private readonly ServerPackageRepository _packageRepository;
private readonly PackageAuthenticationService _packageAuthenticationService;
@@ -24,20 +25,33 @@ public DefaultServiceResolver() : this(
{
}
- public DefaultServiceResolver(string packagePath, NameValueCollection settings)
+ public DefaultServiceResolver(string packagePath, NameValueCollection settings) : this(
+ packagePath,
+ settings,
+ new TraceLogger())
{
+ }
+
+ public DefaultServiceResolver(string packagePath, NameValueCollection settings, Core.Logging.ILogger logger)
+ {
+ _logger = logger;
+
_hashProvider = new CryptoHashProvider(Core.Constants.HashAlgorithm);
_settingsProvider = new WebConfigSettingsProvider(settings);
- _packageRepository = new ServerPackageRepository(packagePath, _hashProvider, _settingsProvider, new TraceLogger());
+ _packageRepository = new ServerPackageRepository(packagePath, _hashProvider, _settingsProvider, _logger);
_packageAuthenticationService = new PackageAuthenticationService(settings);
-
}
public object Resolve(Type type)
{
+ if (type == typeof(Core.Logging.ILogger))
+ {
+ return _logger;
+ }
+
if (type == typeof(IHashProvider))
{
return _hashProvider;
@@ -53,6 +67,11 @@ public object Resolve(Type type)
return _packageAuthenticationService;
}
+ if (type == typeof(ISettingsProvider))
+ {
+ return _settingsProvider;
+ }
+
return null;
}
diff --git a/src/NuGet.Server/Core/Helpers.cs b/src/NuGet.Server/Core/Helpers.cs
index e4bb6d9..424a872 100644
--- a/src/NuGet.Server/Core/Helpers.cs
+++ b/src/NuGet.Server/Core/Helpers.cs
@@ -8,12 +8,12 @@ public static class Helpers
{
public static string GetRepositoryUrl(Uri currentUrl, string applicationPath)
{
- return GetBaseUrl(currentUrl, applicationPath) + "nuget";
+ return GetBaseUrl(currentUrl, applicationPath) + "chocolatey";
}
public static string GetPushUrl(Uri currentUrl, string applicationPath)
{
- return GetBaseUrl(currentUrl, applicationPath) + "nuget";
+ return GetBaseUrl(currentUrl, applicationPath) + "chocolatey";
}
public static string GetBaseUrl(Uri currentUrl, string applicationPath)
diff --git a/src/NuGet.Server/Default.aspx b/src/NuGet.Server/Default.aspx
index ff32180..feeff08 100644
--- a/src/NuGet.Server/Default.aspx
+++ b/src/NuGet.Server/Default.aspx
@@ -34,7 +34,7 @@
<% } %>
- <% if (Request.IsLocal) { %>
+ <% if (Request.IsLocal || ServiceResolver.Current.Resolve().GetBoolSetting("allowRemoteCacheManagement", false)) { %>
-
+
+ ..\..\build
+ $(BUILD_SOURCESDIRECTORY)\build
+ $(NuGetBuildPath)
+ none
+
+
+
-
@@ -191,4 +200,12 @@
REM Rename the Web.config file for packing.
copy $(ProjectDir)Web.config $(TargetDir)Web.config.transform >NUL
-
\ No newline at end of file
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
+
diff --git a/src/NuGet.Server/NuGet.Server.nuspec b/src/NuGet.Server/NuGet.Server.nuspec
index 8549cd9..0f29aed 100644
--- a/src/NuGet.Server/NuGet.Server.nuspec
+++ b/src/NuGet.Server/NuGet.Server.nuspec
@@ -4,11 +4,12 @@
NuGet.Server
$version$
Web Application used to host a simple NuGet feed
- .NET Foundation
- .NET Foundation
- https://raw.githubusercontent.com/NuGet/NuGet.Server/dev/LICENSE.txt
+ Microsoft
+ Microsoft
+ Apache-2.0
https://github.com/NuGet/NuGet.Server
- © .NET Foundation. All rights reserved.
+ © Microsoft Corporation. All rights reserved.
+
diff --git a/src/NuGet.Server/Web.config b/src/NuGet.Server/Web.config
index b084c2c..eda918f 100644
--- a/src/NuGet.Server/Web.config
+++ b/src/NuGet.Server/Web.config
@@ -22,6 +22,12 @@
-->
+
+
+
@@ -56,6 +62,7 @@
Uncomment the following configuration entry to enable NAT support.
-->
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\build
+ $(BUILD_SOURCESDIRECTORY)\build
+ $(NuGetBuildPath)
+ none
+
+
This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
-
\ No newline at end of file
+
diff --git a/test/NuGet.Server.Core.Tests/PackageExtensionsTest.cs b/test/NuGet.Server.Core.Tests/PackageExtensionsTest.cs
index 0782ab7..d55f975 100644
--- a/test/NuGet.Server.Core.Tests/PackageExtensionsTest.cs
+++ b/test/NuGet.Server.Core.Tests/PackageExtensionsTest.cs
@@ -1,6 +1,7 @@
// 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.Linq;
using NuGet.Server.Core.DataServices;
using NuGet.Server.Core.Infrastructure;
@@ -10,6 +11,52 @@ namespace NuGet.Server.Core.Tests
{
public class PackageExtensionsTest
{
+ [Fact]
+ public void AsODataPackage_Uses1900ForUnlistedPublished()
+ {
+ // Arrange
+ var package = new ServerPackage
+ {
+ Version = new SemanticVersion("0.1.0"),
+ Authors = Enumerable.Empty(),
+ Owners = Enumerable.Empty(),
+
+ Listed = false,
+ Created = new DateTimeOffset(2017, 11, 29, 21, 21, 32, TimeSpan.FromHours(-8)),
+ };
+
+ // Act
+ var actual = package.AsODataPackage(ClientCompatibility.Max);
+
+ // Assert
+ Assert.Equal(
+ new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ actual.Published);
+ }
+
+ [Fact]
+ public void AsODataPackage_UsesCreatedForListedPublished()
+ {
+ // Arrange
+ var package = new ServerPackage
+ {
+ Version = new SemanticVersion("0.1.0"),
+ Authors = Enumerable.Empty(),
+ Owners = Enumerable.Empty(),
+
+ Listed = true,
+ Created = new DateTimeOffset(2017, 11, 29, 21, 21, 32, TimeSpan.FromHours(-8)),
+ };
+
+ // Act
+ var actual = package.AsODataPackage(ClientCompatibility.Max);
+
+ // Assert
+ Assert.Equal(
+ new DateTime(2017, 11, 30, 5, 21, 32, DateTimeKind.Utc),
+ actual.Published);
+ }
+
[Theory]
[InlineData(true, true, false, false, 1, true, true)]
[InlineData(false, false, true, true, 1, false, false)]
@@ -38,7 +85,7 @@ public void AsODataPackage_PicksCorrectLatestProperties(
SemVer2IsAbsoluteLatest = v2AbsLatest,
SemVer2IsLatest = v2Latest,
};
- var semVerLevel = new SemanticVersion(level, minor: 0, build: 0, specialVersion: null);
+ var semVerLevel = new SemanticVersion(level, minor: 0, build: 0, specialVersion: null, packageReleaseVersion:0);
// Act
var actual = package.AsODataPackage(new ClientCompatibility(semVerLevel));
diff --git a/test/NuGet.Server.Core.Tests/ServerPackageCacheTest.cs b/test/NuGet.Server.Core.Tests/ServerPackageCacheTest.cs
index 21bf498..02b2a70 100644
--- a/test/NuGet.Server.Core.Tests/ServerPackageCacheTest.cs
+++ b/test/NuGet.Server.Core.Tests/ServerPackageCacheTest.cs
@@ -146,7 +146,7 @@ public void Persist_RetainsSemVer2Version()
{
Id = PackageId,
Version = SemVer2Version
- });
+ }, enableDelisting: false);
// Act
actual.Persist();
@@ -232,7 +232,11 @@ public void Exists_IsCaseInsensitive()
.Setup(x => x.FileExists(CacheFileName))
.Returns(false);
var target = new ServerPackageCache(fileSystem.Object, CacheFileName);
- target.Add(new ServerPackage { Id = "NuGet.Versioning", Version = new SemanticVersion("3.5.0-beta2") });
+ target.Add(new ServerPackage
+ {
+ Id = "NuGet.Versioning",
+ Version = new SemanticVersion("3.5.0-beta2"),
+ }, enableDelisting: false);
// Act
var actual = target.Exists("nuget.versioning", new SemanticVersion("3.5.0-BETA2"));
@@ -250,7 +254,11 @@ public void Exists_ReturnsFalseWhenPackageDoesNotExist()
.Setup(x => x.FileExists(CacheFileName))
.Returns(false);
var target = new ServerPackageCache(fileSystem.Object, CacheFileName);
- target.Add(new ServerPackage { Id = "NuGet.Versioning", Version = new SemanticVersion("3.5.0-beta2") });
+ target.Add(new ServerPackage
+ {
+ Id = "NuGet.Versioning",
+ Version = new SemanticVersion("3.5.0-beta2"),
+ }, enableDelisting: false);
// Act
var actual = target.Exists("NuGet.Frameworks", new SemanticVersion("3.5.0-beta2"));
@@ -268,7 +276,11 @@ public void Exists_ReturnsTrueWhenPackageExists()
.Setup(x => x.FileExists(CacheFileName))
.Returns(false);
var target = new ServerPackageCache(fileSystem.Object, CacheFileName);
- target.Add(new ServerPackage { Id = "NuGet.Versioning", Version = new SemanticVersion("3.5.0-beta2") });
+ target.Add(new ServerPackage
+ {
+ Id = "NuGet.Versioning",
+ Version = new SemanticVersion("3.5.0-beta2"),
+ }, enableDelisting: false);
// Act
var actual = target.Exists("NuGet.Versioning", new SemanticVersion("3.5.0-beta2"));
diff --git a/test/NuGet.Server.Core.Tests/ServerPackageRepositoryTest.cs b/test/NuGet.Server.Core.Tests/ServerPackageRepositoryTest.cs
index ded407a..65e2fee 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);
@@ -106,11 +106,11 @@ public async Task ServerPackageRepositoryAddsPackagesFromDropFolderOnStart(bool
foreach (var packageToAddToDropFolder in packagesToAddToDropFolder)
{
var package = packages.FirstOrDefault(
- p => p.Id == packageToAddToDropFolder.Value.Id
+ p => p.Id == packageToAddToDropFolder.Value.Id
&& p.Version == packageToAddToDropFolder.Value.Version);
// check the package from drop folder has been added
- Assert.NotNull(package);
+ Assert.NotNull(package);
// check the package in the drop folder has been removed
Assert.False(File.Exists(Path.Combine(temporaryDirectory.Path, packageToAddToDropFolder.Key)));
@@ -203,7 +203,7 @@ public async Task ServerPackageRepository_DuplicateAddAfterClearObservesOverride
}
else
{
- await Assert.ThrowsAsync(async () =>
+ await Assert.ThrowsAsync(async () =>
await serverRepository.AddPackageAsync(CreatePackage("test", "1.2"), Token));
}
}
@@ -319,19 +319,10 @@ public async Task ServerPackageRepositorySearchUnlisted()
using (var temporaryDirectory = new TemporaryDirectory())
{
// Arrange
- Func getSetting = (key, defaultValue) =>
- {
- if (key == "enableDelisting")
- {
- return true;
- }
- return defaultValue;
- };
-
var serverRepository = await CreateServerPackageRepositoryAsync(temporaryDirectory.Path, repository =>
{
repository.AddPackage(CreatePackage("test1", "1.0"));
- }, getSetting);
+ }, EnableDelisting);
// Assert base setup
var packages = (await serverRepository.SearchAsync(
@@ -365,6 +356,98 @@ public async Task ServerPackageRepositorySearchUnlisted()
}
}
+ [Fact]
+ public async Task ServerPackageRepositorySearchUnlistingDisabledAndExclude()
+ {
+ await ServerPackageRepositorySearchUnlistedWithOptions(
+ enableUnlisting: false,
+ allowUnlistedVersions: false,
+ searchable: false,
+ gettable: false);
+ }
+
+ [Fact]
+ public async Task ServerPackageRepositorySearchUnlistingDisabledAndInclude()
+ {
+ await ServerPackageRepositorySearchUnlistedWithOptions(
+ enableUnlisting: false,
+ allowUnlistedVersions: true,
+ searchable: false,
+ gettable: false);
+ }
+
+ [Fact]
+ public async Task ServerPackageRepositorySearchUnlistingEnabledAndExclude()
+ {
+ await ServerPackageRepositorySearchUnlistedWithOptions(
+ enableUnlisting: true,
+ allowUnlistedVersions: false,
+ searchable: false,
+ gettable: true);
+ }
+
+ [Fact]
+ public async Task ServerPackageRepositorySearchUnlistingEnabledAndInclude()
+ {
+ await ServerPackageRepositorySearchUnlistedWithOptions(
+ enableUnlisting: true,
+ allowUnlistedVersions: true,
+ searchable: true,
+ gettable: true);
+ }
+
+ private async Task ServerPackageRepositorySearchUnlistedWithOptions(
+ bool enableUnlisting, bool allowUnlistedVersions, bool searchable, bool gettable)
+ {
+ using (var temporaryDirectory = new TemporaryDirectory())
+ {
+ // Arrange
+ var getSetting = enableUnlisting ? EnableDelisting : (Func)null;
+ var serverRepository = await CreateServerPackageRepositoryAsync(temporaryDirectory.Path, repository =>
+ {
+ repository.AddPackage(CreatePackage("test1", "1.0"));
+ }, getSetting);
+
+ // Remove the package
+ await serverRepository.RemovePackageAsync("test1", new SemanticVersion("1.0"), Token);
+
+ // Verify that the package is not returned by search
+ var packages = (await serverRepository.SearchAsync(
+ "test1",
+ allowPrereleaseVersions: true,
+ allowUnlistedVersions: allowUnlistedVersions,
+ compatibility: ClientCompatibility.Max,
+ token: Token)).ToList();
+ if (searchable)
+ {
+ Assert.Equal(1, packages.Count);
+ Assert.Equal("test1", packages[0].Id);
+ Assert.Equal("1.0", packages[0].Version.ToString());
+ Assert.False(packages[0].Listed);
+ }
+ else
+ {
+ Assert.Equal(0, packages.Count);
+ }
+
+ // Act: search with includeDelisted=true
+ packages = (await serverRepository.GetPackagesAsync(ClientCompatibility.Max, Token)).ToList();
+
+ // Assert
+ if (gettable)
+ {
+ Assert.Equal(1, packages.Count);
+ Assert.Equal("test1", packages[0].Id);
+ Assert.Equal("1.0", packages[0].Version.ToString());
+ Assert.False(packages[0].Listed);
+ }
+ else
+ {
+ Assert.Equal(0, packages.Count);
+ }
+ }
+ }
+
[Fact]
public async Task ServerPackageRepositoryFindPackageById()
{
@@ -472,7 +555,7 @@ public async Task ServerPackageRepositoryFindPackage()
new SemanticVersion("1.0.0-alpha"),
Token);
var invalidPreRel = await serverRepository.FindPackageAsync(
- "test3",
+ "test3",
new SemanticVersion("1.0.0"),
Token);
var invalid = await serverRepository.FindPackageAsync("bad", new SemanticVersion("1.0"), Token);
@@ -515,12 +598,15 @@ public async Task ServerPackageRepositoryMultipleIds()
}
}
- [Fact]
- public async Task ServerPackageRepositorySemVer1IsAbsoluteLatest()
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task ServerPackageRepositorySemVer1IsAbsoluteLatest(bool enableDelisting)
{
using (var temporaryDirectory = new TemporaryDirectory())
{
// Arrange
+ var getSetting = enableDelisting ? EnableDelisting : (Func)null;
var serverRepository = await CreateServerPackageRepositoryAsync(temporaryDirectory.Path, repository =>
{
repository.AddPackage(CreatePackage("test", "2.0-alpha"));
@@ -528,8 +614,14 @@ public async Task ServerPackageRepositorySemVer1IsAbsoluteLatest()
repository.AddPackage(CreatePackage("test", "2.2-beta"));
repository.AddPackage(CreatePackage("test", "2.3"));
repository.AddPackage(CreatePackage("test", "2.4.0-prerel"));
+ repository.AddPackage(CreatePackage("test", "2.5.0-prerel"));
repository.AddPackage(CreatePackage("test", "3.2.0+taggedOnly"));
- });
+ }, getSetting);
+
+ await serverRepository.RemovePackageAsync(
+ "test",
+ new SemanticVersion("2.5.0-prerel"),
+ CancellationToken.None);
// Act
var packages = await serverRepository.GetPackagesAsync(ClientCompatibility.Default, Token);
@@ -540,12 +632,15 @@ public async Task ServerPackageRepositorySemVer1IsAbsoluteLatest()
}
}
- [Fact]
- public async Task ServerPackageRepositorySemVer2IsAbsoluteLatest()
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task ServerPackageRepositorySemVer2IsAbsoluteLatest(bool enableDelisting)
{
using (var temporaryDirectory = new TemporaryDirectory())
{
// Arrange
+ var getSetting = enableDelisting ? EnableDelisting : (Func)null;
var serverRepository = await CreateServerPackageRepositoryAsync(temporaryDirectory.Path, repository =>
{
repository.AddPackage(CreatePackage("test", "2.0-alpha"));
@@ -554,7 +649,13 @@ public async Task ServerPackageRepositorySemVer2IsAbsoluteLatest()
repository.AddPackage(CreatePackage("test", "2.3"));
repository.AddPackage(CreatePackage("test", "2.4.0-prerel"));
repository.AddPackage(CreatePackage("test", "3.2.0+taggedOnly"));
- });
+ repository.AddPackage(CreatePackage("test", "3.3.0+unlisted"));
+ }, getSetting);
+
+ await serverRepository.RemovePackageAsync(
+ "test",
+ new SemanticVersion("3.3.0+unlisted"),
+ CancellationToken.None);
// Act
var packages = await serverRepository.GetPackagesAsync(ClientCompatibility.Max, Token);
@@ -577,7 +678,7 @@ public async Task ServerPackageRepositoryIsLatestOnlyPreRel()
repository.AddPackage(CreatePackage("test", "2.1-alpha"));
repository.AddPackage(CreatePackage("test", "2.2-beta+tagged"));
});
-
+
// Act
var packages = await serverRepository.GetPackagesAsync(ClientCompatibility.Max, Token);
@@ -586,18 +687,27 @@ public async Task ServerPackageRepositoryIsLatestOnlyPreRel()
}
}
- [Fact]
- public async Task ServerPackageRepositorySemVer1IsLatest()
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task ServerPackageRepositorySemVer1IsLatest(bool enableDelisting)
{
using (var temporaryDirectory = new TemporaryDirectory())
{
// Arrange
+ var getSetting = enableDelisting ? EnableDelisting : (Func)null;
var serverRepository = await CreateServerPackageRepositoryAsync(temporaryDirectory.Path, repository =>
{
repository.AddPackage(CreatePackage("test1", "1.0.0"));
+ repository.AddPackage(CreatePackage("test1", "1.1.0"));
repository.AddPackage(CreatePackage("test1", "1.2.0+taggedOnly"));
repository.AddPackage(CreatePackage("test1", "2.0.0-alpha"));
- });
+ }, getSetting);
+
+ await serverRepository.RemovePackageAsync(
+ "test1",
+ new SemanticVersion("1.1.0"),
+ CancellationToken.None);
// Act
var packages = await serverRepository.GetPackagesAsync(ClientCompatibility.Default, Token);
@@ -608,21 +718,30 @@ public async Task ServerPackageRepositorySemVer1IsLatest()
}
}
- [Fact]
- public async Task ServerPackageRepositorySemVer2IsLatest()
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task ServerPackageRepositorySemVer2IsLatest(bool enableDelisting)
{
using (var temporaryDirectory = new TemporaryDirectory())
{
// Arrange
+ var getSetting = enableDelisting ? EnableDelisting : (Func)null;
var serverRepository = await CreateServerPackageRepositoryAsync(temporaryDirectory.Path, repository =>
{
repository.AddPackage(CreatePackage("test", "1.11"));
+ repository.AddPackage(CreatePackage("test", "2.0"));
repository.AddPackage(CreatePackage("test", "1.9"));
repository.AddPackage(CreatePackage("test", "2.0-alpha"));
repository.AddPackage(CreatePackage("test1", "1.0.0"));
repository.AddPackage(CreatePackage("test1", "1.2.0+taggedOnly"));
repository.AddPackage(CreatePackage("test1", "2.0.0-alpha"));
- });
+ }, getSetting);
+
+ await serverRepository.RemovePackageAsync(
+ "test",
+ new SemanticVersion("2.0"),
+ CancellationToken.None);
// Act
var packages = await serverRepository.GetPackagesAsync(ClientCompatibility.Max, Token);
@@ -776,7 +895,7 @@ public async Task ServerPackageRepositoryAddPackageRejectsDuplicatesWithSemVer2(
await serverRepository.AddPackageAsync(CreatePackage("Foo", "1.0.0-beta.1+foo"), Token);
// Act & Assert
- var actual = await Assert.ThrowsAsync(async () =>
+ var actual = await Assert.ThrowsAsync(async () =>
await serverRepository.AddPackageAsync(CreatePackage("Foo", "1.0.0-beta.1+bar"), Token));
Assert.Equal(
"Package Foo 1.0.0-beta.1 already exists. The server is configured to not allow overwriting packages that already exist.",
@@ -784,6 +903,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();
@@ -795,7 +991,10 @@ private static IPackage CreateMockPackage(string id, string version)
return package.Object;
}
- private IPackage CreatePackage(string id, string version, PackageDependency packageDependency = null)
+ private IPackage CreatePackage(
+ string id,
+ string version,
+ PackageDependency packageDependency = null)
{
var parsedVersion = new SemanticVersion(version);
var packageBuilder = new PackageBuilder
@@ -848,5 +1047,15 @@ private IPackage CreatePackage(string id, string version, PackageDependency pack
return outputPackage;
}
+
+ private static object EnableDelisting(string key, object defaultValue)
+ {
+ if (key == "enableDelisting")
+ {
+ return true;
+ }
+
+ return defaultValue;
+ }
}
}
diff --git a/test/NuGet.Server.Core.Tests/TestData.cs b/test/NuGet.Server.Core.Tests/TestData.cs
index ee20532..0b4030e 100644
--- a/test/NuGet.Server.Core.Tests/TestData.cs
+++ b/test/NuGet.Server.Core.Tests/TestData.cs
@@ -1,8 +1,10 @@
// 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 System.Reflection;
+using Moq;
namespace NuGet.Server.Core.Tests
{
@@ -36,5 +38,25 @@ public static void CopyResourceToPath(string name, string path)
resourceStream.CopyTo(outputStream);
}
}
+
+ public static Stream GenerateSimplePackage(string id, SemanticVersion version)
+ {
+ var simpleFile = new Mock();
+ simpleFile.Setup(x => x.Path).Returns("file.txt");
+ simpleFile.Setup(x => x.GetStream()).Returns(() => new MemoryStream(new byte[0]));
+
+ var packageBuilder = new PackageBuilder();
+ packageBuilder.Id = id;
+ packageBuilder.Version = version;
+ packageBuilder.Authors.Add("Integration test");
+ packageBuilder.Description = "Simple test package.";
+ packageBuilder.Files.Add(simpleFile.Object);
+
+ var memoryStream = new MemoryStream();
+ packageBuilder.Save(memoryStream);
+ memoryStream.Position = 0;
+
+ return memoryStream;
+ }
}
}
diff --git a/test/NuGet.Server.Core.Tests/TestOutputLogger.cs b/test/NuGet.Server.Core.Tests/TestOutputLogger.cs
new file mode 100644
index 0000000..aad6273
--- /dev/null
+++ b/test/NuGet.Server.Core.Tests/TestOutputLogger.cs
@@ -0,0 +1,36 @@
+// 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.Collections.Concurrent;
+using System.Collections.Generic;
+using NuGet.Server.Core.Logging;
+using Xunit.Abstractions;
+
+namespace NuGet.Server.Core.Tests
+{
+ public class TestOutputLogger : Logging.ILogger
+ {
+ private readonly ITestOutputHelper _output;
+ private ConcurrentQueue _messages;
+
+ public TestOutputLogger(ITestOutputHelper output)
+ {
+ _output = output;
+ _messages = new ConcurrentQueue();
+ }
+
+ public IEnumerable Messages => _messages;
+
+ public void Clear()
+ {
+ _messages = new ConcurrentQueue();
+ }
+
+ public void Log(LogLevel level, string message, params object[] args)
+ {
+ var formattedMessage = $"[{level.ToString().Substring(0, 4).ToUpperInvariant()}] {string.Format(message, args)}";
+ _messages.Enqueue(formattedMessage);
+ _output.WriteLine(formattedMessage);
+ }
+ }
+}
diff --git a/test/NuGet.Server.Tests/IntegrationTests.cs b/test/NuGet.Server.Tests/IntegrationTests.cs
index 477ce90..c86b73e 100644
--- a/test/NuGet.Server.Tests/IntegrationTests.cs
+++ b/test/NuGet.Server.Tests/IntegrationTests.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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;
@@ -6,16 +6,22 @@
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.Logging;
using NuGet.Server.Core.Tests;
using NuGet.Server.Core.Tests.Infrastructure;
+using NuGet.Server.V2;
using Xunit;
+using Xunit.Abstractions;
using ISystemDependencyResolver = System.Web.Http.Dependencies.IDependencyResolver;
using SystemHttpClient = System.Net.Http.HttpClient;
@@ -23,11 +29,18 @@ namespace NuGet.Server.Tests
{
public class IntegrationTests
{
+ private readonly ITestOutputHelper _output;
+
+ public IntegrationTests(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
[Fact]
public async Task DropPackageThenReadPackages()
{
// Arrange
- using (var tc = new TestContext())
+ using (var tc = new TestContext(_output))
{
// Act & Assert
// 1. Get the initial list of packages. This should be empty.
@@ -56,11 +69,98 @@ public async Task DropPackageThenReadPackages()
}
}
+ [Fact]
+ public async Task DownloadPackage()
+ {
+ // Arrange
+ using (var tc = new TestContext(_output))
+ {
+ // 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(_output))
+ {
+ 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 PushDuplicatePackage()
+ {
+ // Arrange
+ using (var tc = new TestContext(_output))
+ {
+ string apiKey = "foobar";
+ tc.SetApiKey(apiKey);
+
+ var packagePath = Path.Combine(tc.TemporaryDirectory, "package.nupkg");
+ TestData.CopyResourceToPath(TestData.PackageResource, packagePath);
+
+ // Act & Assert
+ // 1. Push the package.
+ await tc.PushPackageAsync(apiKey, packagePath);
+
+ // 2. Push the package again expecting a 409 as the Package already exists.
+ await tc.PushPackageAsync(apiKey, packagePath, excepectedStatusCode: HttpStatusCode.Conflict);
+ }
+ }
+
[Fact]
public async Task PushPackageThenReadPackages()
{
// Arrange
- using (var tc = new TestContext())
+ using (var tc = new TestContext(_output))
{
string apiKey = "foobar";
tc.SetApiKey(apiKey);
@@ -70,13 +170,217 @@ public async Task PushPackageThenReadPackages()
// Act & Assert
// 1. Push the package.
+ await tc.PushPackageAsync(apiKey, packagePath);
+
+ // 2. Get the list of packages. This should mention the package.
+ using (var request = new HttpRequestMessage(HttpMethod.Get, "/nuget/Packages()"))
+ 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);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task CanSupportMultipleSetsOfRoutes()
+ {
+ // Arrange
+ using (var tc = new TestContext(_output))
+ {
+ // Enable another set of routes.
+ NuGetV2WebApiEnabler.UseNuGetV2WebApiFeed(
+ tc.Config,
+ "NuGetDefault2",
+ "nuget2",
+ TestablePackagesODataController.Name);
+
+ string apiKey = "foobar";
+ tc.SetApiKey(apiKey);
+
+ var packagePath = Path.Combine(tc.TemporaryDirectory, "package.nupkg");
+ TestData.CopyResourceToPath(TestData.PackageResource, packagePath);
+
+ // Act & Assert
+ // 1. Push to the legacy route.
+ await tc.PushPackageAsync(apiKey, packagePath, "/api/v2/package");
+
+ // 2. Make a request to the first set of routes.
+ using (var request = new HttpRequestMessage(HttpMethod.Get, "/nuget/Packages()"))
+ 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. Make a request to the second set of routes.
+ using (var request = new HttpRequestMessage(HttpMethod.Get, "/nuget2/Packages()"))
+ 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);
+ }
+ }
+ }
+
+ ///
+ /// Added due to https://github.com/NuGet/NuGetGallery/issues/6960. There was a concurrency issue when pushing
+ /// packages that could lead to unnecessary cache rebuilds.
+ ///
+ [Fact]
+ public async Task DoesNotRebuildTheCacheWhenPackagesArePushed()
+ {
+ // Arrange
+ using (var tc = new TestContext(_output))
+ {
+ // Essentially disable the automatic cache rebuild by setting it really far in the future.
+ const int initialCacheRebuildAfterSeconds = 60 * 60 * 24;
+ tc.Settings["initialCacheRebuildAfterSeconds"] = initialCacheRebuildAfterSeconds.ToString();
+
+ const int workerCount = 8;
+ const int totalPackages = 320;
+
+ var packagePaths = new ConcurrentBag();
+ for (var i = 0; i < totalPackages; i++)
+ {
+ var packageId = Guid.NewGuid().ToString();
+ var packagePath = Path.Combine(tc.TemporaryDirectory, $"{packageId}.1.0.0.nupkg");
+ using (var package = TestData.GenerateSimplePackage(packageId, SemanticVersion.Parse("1.0.0")))
+ using (var fileStream = File.OpenWrite(packagePath))
+ {
+ await package.CopyToAsync(fileStream);
+ }
+
+ packagePaths.Add(packagePath);
+ }
+
+ string apiKey = "foobar";
+ tc.SetApiKey(apiKey);
+
+ // Act & Assert
+ // 1. Push a single package to build the cache for the first time.
+ packagePaths.TryTake(out var firstPackagePath);
+ await tc.PushPackageAsync(apiKey, firstPackagePath);
+
+ Assert.Single(tc.Logger.Messages, "[INFO] Start rebuilding package store...");
+ tc.Logger.Clear();
+ tc.TestOutputHelper.WriteLine("The first package has been pushed.");
+
+ // 2. Execute a query to register the file system watcher.
+ using (var request = new HttpRequestMessage(HttpMethod.Get, "/nuget/Packages/$count"))
+ using (var response = await tc.Client.SendAsync(request))
+ {
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var content = await response.Content.ReadAsStringAsync();
+
+ Assert.Equal(1, int.Parse(content));
+ }
+
+ Assert.DoesNotContain("[INFO] Start rebuilding package store...", tc.Logger.Messages);
+ tc.TestOutputHelper.WriteLine("The first count query has completed.");
+
+ // 3. Push the rest of the packages.
+ var workerTasks = Enumerable
+ .Range(0, workerCount)
+ .Select(async i =>
+ {
+ while (packagePaths.TryTake(out var packagePath))
+ {
+ await tc.PushPackageAsync(apiKey, packagePath);
+ }
+ })
+ .ToList();
+ await Task.WhenAll(workerTasks);
+
+ tc.TestOutputHelper.WriteLine("The rest of the packages have been pushed.");
+
+ // 4. Get the total count of packages. This should match the number of packages pushed.
+ using (var request = new HttpRequestMessage(HttpMethod.Get, "/nuget/Packages/$count"))
+ using (var response = await tc.Client.SendAsync(request))
+ {
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var content = await response.Content.ReadAsStringAsync();
+
+ Assert.Equal(totalPackages, int.Parse(content));
+ }
+
+ Assert.DoesNotContain("[INFO] Start rebuilding package store...", tc.Logger.Messages);
+ tc.TestOutputHelper.WriteLine("The second count query has completed.");
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(EndpointsSupportingProjection))]
+ public async Task CanQueryUsingProjection(string endpoint)
+ {
+ // Arrange
+ using (var tc = new TestContext(_output))
+ {
+ var packagePath = Path.Combine(tc.PackagesDirectory, "package.nupkg");
+ TestData.CopyResourceToPath(TestData.PackageResource, packagePath);
+
+ // Act
+ using (var request = new HttpRequestMessage(HttpMethod.Get, $"/nuget/{endpoint}$select=Id,Version"))
+ using (var response = await tc.Client.SendAsync(request))
+ {
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var content = await response.Content.ReadAsStringAsync();
+
+ // Assert
+ Assert.Contains(TestData.PackageId, content);
+ Assert.Contains(TestData.PackageVersionString, content);
+ }
+ }
+ }
+
+ [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(_output))
+ {
+ 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(packagePath)
+ Content = tc.GetFileUploadContent(pushPath),
})
{
using (request)
@@ -86,26 +390,42 @@ public async Task PushPackageThenReadPackages()
}
}
- // 2. Get the list of packages. This should mention the package.
- using (var request = new HttpRequestMessage(HttpMethod.Get, "/nuget/Packages()"))
+ // 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);
- var content = await response.Content.ReadAsStringAsync();
-
- Assert.Contains(TestData.PackageId, content);
}
+
+ // 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