Skip to content
This repository has been archived by the owner on Mar 16, 2021. It is now read-only.

Commit

Permalink
Copy NuGet.Protocol.Catalog from Samples repository (#405)
Browse files Browse the repository at this point in the history
  • Loading branch information
joelverhagen committed Nov 14, 2018
1 parent b87f57b commit 791f129
Show file tree
Hide file tree
Showing 35 changed files with 2,085 additions and 2 deletions.
30 changes: 30 additions & 0 deletions NuGet.Services.Metadata.sln
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CatalogMetadataTests", "tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "V3PerPackage", "src\V3PerPackage\V3PerPackage.csproj", "{E76E73FA-4462-4F07-94C0-8B9CC413F696}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Protocol.Catalog", "src\NuGet.Protocol.Catalog\NuGet.Protocol.Catalog.csproj", "{D44C2E89-2D98-44BD-8712-8CCBE4E67C9C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Protocol.Catalog.Tests", "tests\NuGet.Protocol.Catalog.Tests\NuGet.Protocol.Catalog.Tests.csproj", "{1F3BC053-796C-4A35-88F4-955A0F142197}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -190,6 +194,30 @@ Global
{E76E73FA-4462-4F07-94C0-8B9CC413F696}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{E76E73FA-4462-4F07-94C0-8B9CC413F696}.Release|x64.ActiveCfg = Release|Any CPU
{E76E73FA-4462-4F07-94C0-8B9CC413F696}.Release|x64.Build.0 = Release|Any CPU
{D44C2E89-2D98-44BD-8712-8CCBE4E67C9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D44C2E89-2D98-44BD-8712-8CCBE4E67C9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D44C2E89-2D98-44BD-8712-8CCBE4E67C9C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{D44C2E89-2D98-44BD-8712-8CCBE4E67C9C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{D44C2E89-2D98-44BD-8712-8CCBE4E67C9C}.Debug|x64.ActiveCfg = Debug|Any CPU
{D44C2E89-2D98-44BD-8712-8CCBE4E67C9C}.Debug|x64.Build.0 = Debug|Any CPU
{D44C2E89-2D98-44BD-8712-8CCBE4E67C9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D44C2E89-2D98-44BD-8712-8CCBE4E67C9C}.Release|Any CPU.Build.0 = Release|Any CPU
{D44C2E89-2D98-44BD-8712-8CCBE4E67C9C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{D44C2E89-2D98-44BD-8712-8CCBE4E67C9C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{D44C2E89-2D98-44BD-8712-8CCBE4E67C9C}.Release|x64.ActiveCfg = Release|Any CPU
{D44C2E89-2D98-44BD-8712-8CCBE4E67C9C}.Release|x64.Build.0 = Release|Any CPU
{1F3BC053-796C-4A35-88F4-955A0F142197}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1F3BC053-796C-4A35-88F4-955A0F142197}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1F3BC053-796C-4A35-88F4-955A0F142197}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{1F3BC053-796C-4A35-88F4-955A0F142197}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{1F3BC053-796C-4A35-88F4-955A0F142197}.Debug|x64.ActiveCfg = Debug|Any CPU
{1F3BC053-796C-4A35-88F4-955A0F142197}.Debug|x64.Build.0 = Debug|Any CPU
{1F3BC053-796C-4A35-88F4-955A0F142197}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1F3BC053-796C-4A35-88F4-955A0F142197}.Release|Any CPU.Build.0 = Release|Any CPU
{1F3BC053-796C-4A35-88F4-955A0F142197}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{1F3BC053-796C-4A35-88F4-955A0F142197}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{1F3BC053-796C-4A35-88F4-955A0F142197}.Release|x64.ActiveCfg = Release|Any CPU
{1F3BC053-796C-4A35-88F4-955A0F142197}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -207,6 +235,8 @@ Global
{1745A383-D0BE-484B-81EB-27B20F6AC6C5} = {5DE01C58-D5F7-482F-8256-A8333064384C}
{34AABA7F-1FF7-4F4B-B1DB-D07AD4505DA4} = {F1C83FD9-A498-483E-ADFA-B55D82A14965}
{E76E73FA-4462-4F07-94C0-8B9CC413F696} = {C86C6DEE-84E1-4E4E-8868-6755D7A8E0E4}
{D44C2E89-2D98-44BD-8712-8CCBE4E67C9C} = {5DE01C58-D5F7-482F-8256-A8333064384C}
{1F3BC053-796C-4A35-88F4-955A0F142197} = {F1C83FD9-A498-483E-ADFA-B55D82A14965}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D3AB83E9-02B4-4FFA-A2D0-637F0B97E626}
Expand Down
3 changes: 2 additions & 1 deletion build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ Invoke-BuildStep 'Set version metadata in AssemblyInfo.cs' {
"src\Catalog\Properties\AssemblyInfo.g.cs", `
"src\NuGet.ApplicationInsights.Owin\Properties\AssemblyInfo.g.cs", `
"src\Ng\Properties\AssemblyInfo.g.cs", `
"src\NuGet.Services.Metadata.Catalog.Monitoring\Properties\AssemblyInfo.g.cs"
"src\NuGet.Services.Metadata.Catalog.Monitoring\Properties\AssemblyInfo.g.cs", `
"src\NuGet.Protocol.Catalog\Properties\AssemblyInfo.g.cs"

Foreach ($assemblyInfo in $assemblyInfos) {
Set-VersionInfo -Path (Join-Path $PSScriptRoot $assemblyInfo) -Version $SimpleVersion -Branch $Branch -Commit $CommitSHA
Expand Down
113 changes: 113 additions & 0 deletions src/NuGet.Protocol.Catalog/CatalogClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// 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.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace NuGet.Protocol.Catalog
{
public class CatalogClient : ICatalogClient
{
private static readonly JsonSerializer JsonSerializer = CatalogJsonSerialization.Serializer;
private readonly HttpClient _httpClient;
private readonly ILogger<CatalogClient> _logger;

public CatalogClient(HttpClient httpClient, ILogger<CatalogClient> logger)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

public Task<CatalogIndex> GetIndexAsync(string indexUrl)
{
return DeserializeUrlAsync<CatalogIndex>(indexUrl);
}

public Task<CatalogPage> GetPageAsync(string pageUrl)
{
return DeserializeUrlAsync<CatalogPage>(pageUrl);
}

public async Task<CatalogLeaf> GetLeafAsync(string leafUrl)
{
// Buffer all of the JSON so we can parse twice. Once to determine the leaf type and once to deserialize
// the entire thing to the proper leaf type.
_logger.LogDebug("Downloading {leafUrl} as a byte array.", leafUrl);
var jsonBytes = await _httpClient.GetByteArrayAsync(leafUrl);
var untypedLeaf = DeserializeBytes<CatalogLeaf>(jsonBytes);

switch (untypedLeaf.Type)
{
case CatalogLeafType.PackageDetails:
return DeserializeBytes<PackageDetailsCatalogLeaf>(jsonBytes);
case CatalogLeafType.PackageDelete:
return DeserializeBytes<PackageDeleteCatalogLeaf>(jsonBytes);
default:
throw new NotSupportedException($"The catalog leaf type '{untypedLeaf.Type}' is not supported.");
}
}

private async Task<CatalogLeaf> GetLeafAsync(CatalogLeafType type, string leafUrl)
{
switch (type)
{
case CatalogLeafType.PackageDetails:
return await GetPackageDetailsLeafAsync(leafUrl);
case CatalogLeafType.PackageDelete:
return await GetPackageDeleteLeafAsync(leafUrl);
default:
throw new NotSupportedException($"The catalog leaf type '{type}' is not supported.");
}
}

public Task<PackageDeleteCatalogLeaf> GetPackageDeleteLeafAsync(string leafUrl)
{
return GetAndValidateLeafAsync<PackageDeleteCatalogLeaf>(CatalogLeafType.PackageDelete, leafUrl);
}

public Task<PackageDetailsCatalogLeaf> GetPackageDetailsLeafAsync(string leafUrl)
{
return GetAndValidateLeafAsync<PackageDetailsCatalogLeaf>(CatalogLeafType.PackageDetails, leafUrl);
}

private async Task<T> GetAndValidateLeafAsync<T>(CatalogLeafType type, string leafUrl) where T : CatalogLeaf
{
var leaf = await DeserializeUrlAsync<T>(leafUrl);

if (leaf.Type != type)
{
throw new ArgumentException(
$"The leaf type found in the document does not match the expected '{type}' type.",
nameof(type));
}

return leaf;
}

private T DeserializeBytes<T>(byte[] jsonBytes)
{
using (var stream = new MemoryStream(jsonBytes))
using (var textReader = new StreamReader(stream))
using (var jsonReader = new JsonTextReader(textReader))
{
return JsonSerializer.Deserialize<T>(jsonReader);
}
}

private async Task<T> DeserializeUrlAsync<T>(string documentUrl)
{
_logger.LogDebug("Downloading {documentUrl} as a stream.", documentUrl);

using (var stream = await _httpClient.GetStreamAsync(documentUrl))
using (var textReader = new StreamReader(stream))
using (var jsonReader = new JsonTextReader(textReader))
{
return JsonSerializer.Deserialize<T>(jsonReader);
}
}
}
}
218 changes: 218 additions & 0 deletions src/NuGet.Protocol.Catalog/CatalogProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
// 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.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NuGet.Protocol.Core.Types;

namespace NuGet.Protocol.Catalog
{
public class CatalogProcessor
{
private const string CatalogResourceType = "Catalog/3.0.0";
private readonly ICatalogLeafProcessor _leafProcessor;
private readonly ICatalogClient _client;
private readonly ICursor _cursor;
private readonly ILogger<CatalogProcessor> _logger;
private readonly CatalogProcessorSettings _settings;

public CatalogProcessor(
ICursor cursor,
ICatalogClient client,
ICatalogLeafProcessor leafProcessor,
CatalogProcessorSettings settings,
ILogger<CatalogProcessor> logger)
{
_leafProcessor = leafProcessor ?? throw new ArgumentNullException(nameof(leafProcessor));
_client = client ?? throw new ArgumentNullException(nameof(client));
_cursor = cursor ?? throw new ArgumentNullException(nameof(cursor));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));

if (settings == null)
{
throw new ArgumentNullException(nameof(settings));
}

if (settings.ServiceIndexUrl == null)
{
throw new ArgumentException(
$"The {nameof(CatalogProcessorSettings.ServiceIndexUrl)} property of the " +
$"{nameof(CatalogProcessorSettings)} must not be null.",
nameof(settings));
}

// Clone the settings to avoid mutability issues.
_settings = settings.Clone();
}

/// <summary>
/// Discovers and downloads all of the catalog leafs after the current cursor value and before the maximum
/// commit timestamp found in the settings. Each catalog leaf is passed to the catalog leaf processor in
/// chronological order. After a commit is completed, its commit timestamp is written to the cursor, i.e. when
/// transitioning from commit timestamp A to B, A is written to the cursor so that it never is processed again.
/// </summary>
/// <returns>True if all of the catalog leaves found were processed successfully.</returns>
public async Task<bool> ProcessAsync()
{
var catalogIndexUrl = await GetCatalogIndexUrlAsync();

var minCommitTimestamp = await GetMinCommitTimestamp();
_logger.LogInformation(
"Using time bounds {min:O} (exclusive) to {max:O} (inclusive).",
minCommitTimestamp,
_settings.MaxCommitTimestamp);

return await ProcessIndexAsync(catalogIndexUrl, minCommitTimestamp);
}

private async Task<bool> ProcessIndexAsync(string catalogIndexUrl, DateTimeOffset minCommitTimestamp)
{
var index = await _client.GetIndexAsync(catalogIndexUrl);

var pageItems = index.GetPagesInBounds(
minCommitTimestamp,
_settings.MaxCommitTimestamp);
_logger.LogInformation(
"{pages} pages were in the time bounds, out of {totalPages}.",
pageItems.Count,
index.Items.Count);

var success = true;
for (var i = 0; i < pageItems.Count; i++)
{
success = await ProcessPageAsync(minCommitTimestamp, pageItems[i]);
if (!success)
{
_logger.LogWarning(
"{unprocessedPages} out of {pages} pages were left incomplete due to a processing failure.",
pageItems.Count - i,
pageItems.Count);
break;
}
}

return success;
}

private async Task<bool> ProcessPageAsync(DateTimeOffset minCommitTimestamp, CatalogPageItem pageItem)
{
var page = await _client.GetPageAsync(pageItem.Url);

var leafItems = page.GetLeavesInBounds(
minCommitTimestamp,
_settings.MaxCommitTimestamp,
_settings.ExcludeRedundantLeaves);
_logger.LogInformation(
"On page {page}, {leaves} out of {totalLeaves} were in the time bounds.",
pageItem.Url,
leafItems.Count,
page.Items.Count);

DateTimeOffset? newCursor = null;
var success = true;
for (var i = 0; i < leafItems.Count; i++)
{
var leafItem = leafItems[i];

if (newCursor.HasValue && newCursor.Value != leafItem.CommitTimestamp)
{
await _cursor.SetAsync(newCursor.Value);
}

newCursor = leafItem.CommitTimestamp;

success = await ProcessLeafAsync(leafItem);
if (!success)
{
_logger.LogWarning(
"{unprocessedLeaves} out of {leaves} leaves were left incomplete due to a processing failure.",
leafItems.Count - i,
leafItems.Count);
break;
}
}

if (newCursor.HasValue && success)
{
await _cursor.SetAsync(newCursor.Value);
}

return success;
}

private async Task<bool> ProcessLeafAsync(CatalogLeafItem leafItem)
{
bool success;
try
{
switch (leafItem.Type)
{
case CatalogLeafType.PackageDelete:
var packageDelete = await _client.GetPackageDeleteLeafAsync(leafItem.Url);
success = await _leafProcessor.ProcessPackageDeleteAsync(packageDelete);
break;
case CatalogLeafType.PackageDetails:
var packageDetails = await _client.GetPackageDetailsLeafAsync(leafItem.Url);
success = await _leafProcessor.ProcessPackageDetailsAsync(packageDetails);
break;
default:
throw new NotSupportedException($"The catalog leaf type '{leafItem.Type}' is not supported.");
}
}
catch (Exception exception)
{
_logger.LogError(
0,
exception,
"An exception was thrown while processing leaf {leafUrl}.",
leafItem.Url);
success = false;
}

if (!success)
{
_logger.LogWarning(
"Failed to process leaf {leafUrl} ({packageId} {packageVersion}, {leafType}).",
leafItem.Url,
leafItem.PackageId,
leafItem.PackageVersion,
leafItem.Type);
}

return success;
}

private async Task<DateTimeOffset> GetMinCommitTimestamp()
{
var minCommitTimestamp = await _cursor.GetAsync();

minCommitTimestamp = minCommitTimestamp
?? _settings.DefaultMinCommitTimestamp
?? _settings.MinCommitTimestamp;

if (minCommitTimestamp.Value < _settings.MinCommitTimestamp)
{
minCommitTimestamp = _settings.MinCommitTimestamp;
}

return minCommitTimestamp.Value;
}

private async Task<string> GetCatalogIndexUrlAsync()
{
_logger.LogInformation("Getting catalog index URL from {serviceIndexUrl}.", _settings.ServiceIndexUrl);
string catalogIndexUrl;
var sourceRepository = Repository.Factory.GetCoreV3(_settings.ServiceIndexUrl, FeedType.HttpV3);
var serviceIndexResource = await sourceRepository.GetResourceAsync<ServiceIndexResourceV3>();
catalogIndexUrl = serviceIndexResource.GetServiceEntryUri(CatalogResourceType)?.AbsoluteUri;
if (catalogIndexUrl == null)
{
throw new InvalidOperationException(
$"The service index does not contain resource '{CatalogResourceType}'.");
}

return catalogIndexUrl;
}
}
}
Loading

0 comments on commit 791f129

Please sign in to comment.