Skip to content

Commit

Permalink
Add ReadmeUriTemplateResource for download README from remote sources. (
Browse files Browse the repository at this point in the history
#6136)

* Added resource type for loading from server

* Add loading message

* Add unit tests

* Fix comment, add check for null

* remove call to GetReadmeUrl

* remove unused using

* update test to reflect new resource provider count

* add nullable enabled

* Added comments

* Fixes from PR

* Switch load

* Pr comments

* Ensure error loading is false when starting to load readme

* Rename ErrorLoadingReadme to ErrorWithReadme
  • Loading branch information
jgonz120 authored Nov 23, 2024
1 parent 3332ab2 commit c0d3837
Show file tree
Hide file tree
Showing 15 changed files with 323 additions and 42 deletions.
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));
}
}
}
4 changes: 2 additions & 2 deletions src/NuGet.Core/NuGet.Protocol/Model/PackageSearchMetadata.cs
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

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
{
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());
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
{
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

0 comments on commit c0d3837

Please sign in to comment.