Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ReadmeUriTemplateResource for download README from remote sources. #6136

Merged
merged 14 commits into from
Nov 23, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ private void DetailControlModel_PropertyChanged(object sender, PropertyChangedEv
{
NuGetUIThreadHelper.JoinableTaskFactory.RunAsync(async () =>
{
if (_readmeTabEnabled)
if (_readmeTabEnabled && e.PropertyName == nameof(DetailControlModel.PackageMetadata))
{
await ReadmePreviewViewModel.SetPackageMetadataAsync(DetailControlModel.PackageMetadata, CancellationToken.None);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,46 @@ namespace NuGet.PackageManagement.UI.ViewModels
{
public sealed class ReadmePreviewViewModel : TitledPageViewModelBase
{
private bool _errorLoadingReadme;
private bool _errorWithReadme;
private INuGetPackageFileService _nugetPackageFileService;
private string _rawReadme;
private DetailedPackageMetadata _packageMetadata;
private bool _canRenderLocalReadme;
private bool _isBusy;

public ReadmePreviewViewModel(INuGetPackageFileService packageFileService, ItemFilter itemFilter, bool isReadmeFeatureEnabled)
{
_nugetPackageFileService = packageFileService ?? throw new ArgumentNullException(nameof(packageFileService));
_canRenderLocalReadme = CanRenderLocalReadme(itemFilter);
_nugetPackageFileService = packageFileService;
_errorLoadingReadme = false;
_errorWithReadme = false;
_isBusy = false;
_rawReadme = string.Empty;
_packageMetadata = null;
Title = Resources.Label_Readme_Tab;
IsVisible = isReadmeFeatureEnabled;
}

public bool ErrorLoadingReadme
public bool IsReadmeReady { get => !IsBusy && !ErrorWithReadme; }

public bool ErrorWithReadme
{
get => _errorLoadingReadme;
set => SetAndRaisePropertyChanged(ref _errorLoadingReadme, value);
get => _errorWithReadme;
set
{
SetAndRaisePropertyChanged(ref _errorWithReadme, value);
RaisePropertyChanged(nameof(IsReadmeReady));
}
}

public bool IsBusy
{
get => _isBusy;
set
{
SetAndRaisePropertyChanged(ref _isBusy, value);
RaisePropertyChanged(nameof(IsReadmeReady));
}
}

public string ReadmeMarkdown
Expand Down Expand Up @@ -76,7 +94,7 @@ private async Task LoadReadmeAsync(CancellationToken cancellationToken)
{
ReadmeMarkdown = _canRenderLocalReadme && !string.IsNullOrWhiteSpace(_packageMetadata.PackagePath) ? Resources.Text_NoReadme : string.Empty;
IsVisible = !string.IsNullOrWhiteSpace(ReadmeMarkdown);
ErrorLoadingReadme = false;
ErrorWithReadme = false;
return;
}

Expand All @@ -85,29 +103,34 @@ private async Task LoadReadmeAsync(CancellationToken cancellationToken)
{
ReadmeMarkdown = string.Empty;
IsVisible = false;
ErrorLoadingReadme = false;
ErrorWithReadme = false;
return;
}

var readme = Resources.Text_NoReadme;
await ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
try
{
await TaskScheduler.Default;
using var readmeStream = await _nugetPackageFileService.GetReadmeAsync(readmeUrl, cancellationToken);
if (readmeStream is null)
IsBusy = true;
ErrorWithReadme = false;
await ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
{
return;
}
await TaskScheduler.Default;
using var readmeStream = await _nugetPackageFileService.GetReadmeAsync(readmeUrl, cancellationToken);
if (readmeStream is null)
{
return;
}

using StreamReader streamReader = new StreamReader(readmeStream);
readme = await streamReader.ReadToEndAsync();
});

if (!cancellationToken.IsCancellationRequested)
using StreamReader streamReader = new StreamReader(readmeStream);
readme = await streamReader.ReadToEndAsync();
});
}
finally
{
ReadmeMarkdown = readme;
IsVisible = !string.IsNullOrWhiteSpace(readme);
ErrorLoadingReadme = false;
ErrorWithReadme = false;
IsBusy = false;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
xmlns:imagingTheme="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Imaging"
xmlns:catalog="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.ImageCatalog"
xmlns:ui="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Utilities"
Background="{DynamicResource {x:Static nuget:Brushes.DetailPaneBackground}}"
Foreground="{DynamicResource {x:Static nuget:Brushes.UIText}}"
DataContextChanged="UserControl_DataContextChanged"
Unloaded="PackageReadmeControl_Unloaded"
Loaded="PackageReadmeControl_Loaded"
Expand All @@ -28,12 +30,14 @@
x:Uid="descriptionMarkdownPreview"
ClipToBounds="True"
Focusable="False"
Visibility="{Binding Path=ErrorLoadingReadme, Mode=OneWay, Converter={StaticResource NegatedBooleanToVisibilityConverter}}"
/>
Visibility="{Binding Path=IsReadmeReady, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}" />
<TextBlock
TextWrapping="Wrap"
Text="{x:Static nuget:Resources.Text_Loading}"
Visibility="{Binding Path=IsBusy, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}" />
<TextBlock
TextWrapping="Wrap"
Text="{x:Static nuget:Resources.Error_UnableToLoadReadme}"
Visibility="{Binding Path=ErrorLoadingReadme, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"
/>
Visibility="{Binding Path=ErrorWithReadme, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}" />
</Grid>
</UserControl>
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using System.Windows;
using System.Windows.Controls;
using Microsoft.VisualStudio.Markdown.Platform;
using Microsoft.VisualStudio.Shell;
using NuGet.PackageManagement.UI.ViewModels;
using NuGet.VisualStudio;
using NuGet.VisualStudio.Telemetry;
Expand Down Expand Up @@ -39,7 +38,7 @@ private void ReadmeViewModel_PropertyChanged(object sender, PropertyChangedEvent
{
if (e.PropertyName == nameof(ReadmePreviewViewModel.ReadmeMarkdown))
{
NuGetUIThreadHelper.JoinableTaskFactory.Run(UpdateMarkdownAsync);
NuGetUIThreadHelper.JoinableTaskFactory.RunAsync(UpdateMarkdownAsync).PostOnFailure(nameof(PackageReadmeControl), nameof(ReadmeViewModel_PropertyChanged));
}
}

Expand Down Expand Up @@ -79,7 +78,7 @@ private async Task UpdateMarkdownAsync()
}
catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException)
{
ReadmeViewModel.ErrorLoadingReadme = true;
ReadmeViewModel.ErrorWithReadme = true;
ReadmeViewModel.ReadmeMarkdown = string.Empty;
await TelemetryUtility.PostFaultAsync(ex, nameof(ReadmePreviewViewModel));
}
Expand All @@ -104,10 +103,8 @@ private void PackageReadmeControl_Unloaded(object sender, RoutedEventArgs e)

private void PackageReadmeControl_Loaded(object sender, RoutedEventArgs e)
{
ThreadHelper.JoinableTaskFactory.Run(async () =>
{
await UpdateMarkdownAsync();
});
NuGetUIThreadHelper.JoinableTaskFactory.RunAsync(UpdateMarkdownAsync)
.PostOnFailure(nameof(PackageReadmeControl), nameof(PackageReadmeControl_Loaded));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ public string Owners
[JsonConverter(typeof(SafeUriConverter))]
public Uri ReadmeUrl { get; private set; }

[JsonProperty(PropertyName = JsonProperties.ReadmeFileUrl)]
public string ReadmeFileUrl { get; private set; }
[JsonIgnore]
public string ReadmeFileUrl { get; internal set; }

[JsonIgnore]
public Uri ReportAbuseUrl { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public override async Task<Tuple<bool, INuGetResource>> TryCreate(SourceReposito
{
var regResource = await source.GetResourceAsync<RegistrationResourceV3>(token);
var reportAbuseResource = await source.GetResourceAsync<ReportAbuseResourceV3>(token);
var readmeResource = await source.GetResourceAsync<ReadmeUriTemplateResource>(token);
var packageDetailsUriResource = await source.GetResourceAsync<PackageDetailsUriResourceV3>(token);

var httpSourceResource = await source.GetResourceAsync<HttpSourceResource>(token);
Expand All @@ -32,7 +33,8 @@ public override async Task<Tuple<bool, INuGetResource>> TryCreate(SourceReposito
httpSourceResource.HttpSource,
regResource,
reportAbuseResource,
packageDetailsUriResource);
packageDetailsUriResource,
readmeResource);
}

return new Tuple<bool, INuGetResource>(curResource != null, curResource);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#nullable enable

jgonz120 marked this conversation as resolved.
Show resolved Hide resolved
using System;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Protocol.Core.Types;

namespace NuGet.Protocol
{
/// <summary>NuGet.Protocol resource provider for <see cref="ReadmeUriTemplateResource"/>.</summary>
/// <remarks>When successful, returns an instance of <see cref="ReadmeUriTemplateResource"/>.</remarks>
internal class ReadmeUriTemplateResourceProvider : ResourceProvider
jgonz120 marked this conversation as resolved.
Show resolved Hide resolved
{
public ReadmeUriTemplateResourceProvider()
: base(typeof(ReadmeUriTemplateResource),
nameof(ReadmeUriTemplateResource),
NuGetResourceProviderPositions.Last)
{
}

/// <inheritdoc cref="ResourceProvider.TryCreate(SourceRepository, CancellationToken)"/>
public override async Task<Tuple<bool, INuGetResource?>> TryCreate(SourceRepository source, CancellationToken token)
{
ReadmeUriTemplateResource? resource = null;
var serviceIndex = await source.GetResourceAsync<ServiceIndexResourceV3>(token);
if (serviceIndex != null)
{
var uriTemplate = serviceIndex.GetServiceEntryUri(ServiceTypes.ReadmeFileUrl)?.OriginalString;

// construct a new resource
resource = string.IsNullOrWhiteSpace(uriTemplate) ? null : new ReadmeUriTemplateResource(uriTemplate);
}

return new Tuple<bool, INuGetResource?>(resource != null, resource);
}
}
}
1 change: 1 addition & 0 deletions src/NuGet.Core/NuGet.Protocol/Repository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public virtual IEnumerable<Lazy<INuGetResourceProvider>> GetCoreV3()
yield return new Lazy<INuGetResourceProvider>(() => new RegistrationResourceV3Provider());
yield return new Lazy<INuGetResourceProvider>(() => new SymbolPackageUpdateResourceV3Provider());
yield return new Lazy<INuGetResourceProvider>(() => new ReportAbuseResourceV3Provider());
yield return new Lazy<INuGetResourceProvider>(() => new ReadmeUriTemplateResourceProvider());
jgonz120 marked this conversation as resolved.
Show resolved Hide resolved
yield return new Lazy<INuGetResourceProvider>(() => new PackageDetailsUriResourceV3Provider());
yield return new Lazy<INuGetResourceProvider>(() => new ServiceIndexResourceV3Provider());
yield return new Lazy<INuGetResourceProvider>(() => new ODataServiceDocumentResourceV2Provider());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class PackageMetadataResourceV3 : PackageMetadataResource
{
private readonly RegistrationResourceV3 _regResource;
private readonly ReportAbuseResourceV3 _reportAbuseResource;
private readonly ReadmeUriTemplateResource _readmeUriTemplateResource;
private readonly PackageDetailsUriResourceV3 _packageDetailsUriResource;
private readonly HttpSource _client;

Expand All @@ -36,6 +37,16 @@ public PackageMetadataResourceV3(
_packageDetailsUriResource = packageDetailsUriResource;
}

internal PackageMetadataResourceV3(
HttpSource client,
RegistrationResourceV3 regResource,
ReportAbuseResourceV3 reportAbuseResource,
PackageDetailsUriResourceV3 packageDetailsUriResource,
ReadmeUriTemplateResource readmeResource) : this(client, regResource, reportAbuseResource, packageDetailsUriResource)
{
_readmeUriTemplateResource = readmeResource;
}

/// <param name="packageId">PackageId for package we're looking.</param>
/// <param name="includePrerelease">Whether to include PreRelease versions into result.</param>
/// <param name="includeUnlisted">Whether to include Unlisted versions into result.</param>
Expand Down Expand Up @@ -269,6 +280,10 @@ private void ProcessRegistrationPage(
{
catalogEntry.ReportAbuseUrl = _reportAbuseResource?.GetReportAbuseUrl(catalogEntry.PackageId, catalogEntry.Version);
catalogEntry.PackageDetailsUrl = _packageDetailsUriResource?.GetUri(catalogEntry.PackageId, catalogEntry.Version);
if (string.IsNullOrWhiteSpace(catalogEntry.ReadmeFileUrl))
{
catalogEntry.ReadmeFileUrl = _readmeUriTemplateResource?.GetReadmeUrl(catalogEntry.PackageId, catalogEntry.Version);
}
catalogEntry = metadataCache.GetObject(catalogEntry);
results.Add(catalogEntry);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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 NuGet.Protocol.Core.Types;
using NuGet.Versioning;

#if NETCOREAPP
using System;
#endif

namespace NuGet.Protocol
{
/// <summary>
/// A resource that provides the URI for downloading a README file based on a template.
/// </summary>
internal class ReadmeUriTemplateResource : INuGetResource
jgonz120 marked this conversation as resolved.
Show resolved Hide resolved
{
private readonly string _uriTemplate;
private const string LowerId = "{lower_id}";
private const string LowerVersion = "{lower_version}";

public ReadmeUriTemplateResource(string uriTemplate)
{
_uriTemplate = uriTemplate;
}

/// <summary>
/// Get the URL for downloading the readme file.
/// </summary>
/// <param name="id">The package id</param>
/// <param name="version">The package version</param>
/// <returns>URL to download README, built using the URI template.</returns>
public string GetReadmeUrl(string id, NuGetVersion version)
{
if (_uriTemplate == null)
{
return string.Empty;
}

var uriString = _uriTemplate
#if NETCOREAPP
.Replace(LowerId, id.ToLowerInvariant(), StringComparison.OrdinalIgnoreCase)
.Replace(LowerVersion, version.ToNormalizedString().ToLowerInvariant(), StringComparison.OrdinalIgnoreCase);
#else
.Replace(LowerId, id.ToLowerInvariant())
.Replace(LowerVersion, version.ToNormalizedString().ToLowerInvariant());
#endif

return uriString;
}
}
}
2 changes: 2 additions & 0 deletions src/NuGet.Core/NuGet.Protocol/ServiceTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ public static class ServiceTypes
public static readonly string Version510 = "/5.1.0";
internal const string Version670 = "/6.7.0";
internal const string Version6110 = "/6.11.0";
internal const string Version6130 = "/6.13.0";

public static readonly string[] SearchQueryService = { "SearchQueryService" + Versioned, "SearchQueryService" + Version340, "SearchQueryService" + Version300beta };
public static readonly string[] RegistrationsBaseUrl = { $"RegistrationsBaseUrl{Versioned}", $"RegistrationsBaseUrl{Version360}", $"RegistrationsBaseUrl{Version340}", $"RegistrationsBaseUrl{Version300rc}", $"RegistrationsBaseUrl{Version300beta}", "RegistrationsBaseUrl" };
public static readonly string[] SearchAutocompleteService = { "SearchAutocompleteService" + Versioned, "SearchAutocompleteService" + Version300beta };
public static readonly string[] ReportAbuse = { "ReportAbuseUriTemplate" + Versioned, "ReportAbuseUriTemplate" + Version300 };
internal static readonly string[] ReadmeFileUrl = { "ReadmeUriTemplate" + Versioned, "ReadmeUriTemplate" + Version6130 };
public static readonly string[] PackageDetailsUriTemplate = { "PackageDetailsUriTemplate" + Version510 };
public static readonly string[] LegacyGallery = { "LegacyGallery" + Versioned, "LegacyGallery" + Version200 };
public static readonly string[] PackagePublish = { "PackagePublish" + Versioned, "PackagePublish" + Version200 };
Expand Down
Loading