Skip to content

Commit 9eede69

Browse files
robertmuehsigjoelverhagen
authored andcommitted
Introduce an Atom feed for packages (#6891)
* copied from the old PR in panic, but should be "cleaner" (at least with the latest master changes) * copied from the old PR in panic, but should be "cleaner" (at least with the latest master changes) * revert applicationhost.config to clean state * avoid using title and published, instead use id and created and * copied from the old PR in panic, but should be "cleaner" (at least with the latest master changes) * copied from the old PR in panic, but should be "cleaner" (at least with the latest master changes) * avoid using title and published, instead use id and created and * unwanted merge change * Resolve #2992
1 parent fd7b1bb commit 9eede69

9 files changed

Lines changed: 362 additions & 0 deletions

File tree

src/NuGetGallery/App_Start/Routes.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,15 @@ public static void RegisterUIRoutes(RouteCollection routes)
199199
},
200200
new { version = new VersionRouteConstraint() });
201201

202+
routes.MapRoute(
203+
RouteName.DisplayPackageFeed,
204+
"packages/{id}/atom.xml",
205+
new
206+
{
207+
controller = "Packages",
208+
action = nameof(PackagesController.AtomFeed)
209+
});
210+
202211
routes.MapRoute(
203212
RouteName.PackageEnableLicenseReport,
204213
"packages/{id}/{version}/EnableLicenseReport",
782 Bytes
Loading

src/NuGetGallery/Controllers/PackagesController.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Linq;
1010
using System.Net;
1111
using System.Net.Mail;
12+
using System.ServiceModel.Syndication;
1213
using System.Text;
1314
using System.Threading.Tasks;
1415
using System.Web;
@@ -27,6 +28,7 @@
2728
using NuGetGallery.Diagnostics;
2829
using NuGetGallery.Filters;
2930
using NuGetGallery.Helpers;
31+
using NuGetGallery.Infrastructure;
3032
using NuGetGallery.Infrastructure.Lucene;
3133
using NuGetGallery.Infrastructure.Mail.Messages;
3234
using NuGetGallery.Infrastructure.Mail.Requests;
@@ -734,6 +736,78 @@ public virtual async Task<ActionResult> DisplayPackage(string id, string version
734736
return View(model);
735737
}
736738

739+
[HttpGet]
740+
public virtual ActionResult AtomFeed(string id, bool allowPrerelease = false)
741+
{
742+
var packageRegistration = _packageService.FindPackageRegistrationById(id);
743+
if (packageRegistration == null)
744+
{
745+
return HttpNotFound();
746+
}
747+
748+
var packageVersions = packageRegistration.Packages
749+
.Where(x => x.Listed && x.PackageStatusKey == PackageStatus.Available)
750+
.OrderByDescending(p => NuGetVersion.Parse(p.NormalizedVersion))
751+
.ToList();
752+
753+
if (packageVersions.Count == 0)
754+
{
755+
return HttpNotFound();
756+
}
757+
758+
// most recent version for feed title/description
759+
var newestVersionPackage = packageVersions.First();
760+
761+
// the last edited or created package is used as the feed timestamp
762+
var lastUpdatedPackage = packageVersions.Max(x => x.LastEdited ?? x.Created);
763+
764+
SyndicationFeed feed = new SyndicationFeed()
765+
{
766+
Id = Url.Package(packageRegistration.Id, version: null, relativeUrl: false),
767+
Title = SyndicationContent.CreatePlaintextContent($"{_config.Brand} Feed for {packageRegistration.Id}"),
768+
Description = SyndicationContent.CreatePlaintextContent(newestVersionPackage.Description),
769+
LastUpdatedTime = lastUpdatedPackage
770+
};
771+
772+
if (!string.IsNullOrWhiteSpace(newestVersionPackage.IconUrl))
773+
{
774+
feed.ImageUrl = new Uri(newestVersionPackage.IconUrl);
775+
}
776+
777+
List<SyndicationItem> feedItems = new List<SyndicationItem>();
778+
779+
List<SyndicationPerson> ownersAsAuthors = new List<SyndicationPerson>();
780+
foreach (var packageOwner in packageRegistration.Owners)
781+
{
782+
ownersAsAuthors.Add(new SyndicationPerson() { Name = packageOwner.Username, Uri = Url.User(packageOwner, relativeUrl: false) });
783+
}
784+
785+
foreach (var packageVersion in packageVersions)
786+
{
787+
SyndicationItem syndicationItem = new SyndicationItem($"{packageVersion.Id} {packageVersion.Version}",
788+
packageVersion.Description,
789+
new Uri(Url.Package(packageRegistration.Id, version: packageVersion.Version, relativeUrl: false)));
790+
syndicationItem.Id = Url.Package(packageRegistration.Id, version: packageVersion.Version, relativeUrl: false);
791+
syndicationItem.LastUpdatedTime = packageVersion.LastEdited ?? packageVersion.Created;
792+
syndicationItem.PublishDate = packageVersion.Created;
793+
794+
syndicationItem.Authors.AddRange(ownersAsAuthors);
795+
feedItems.Add(syndicationItem);
796+
}
797+
798+
feed.Items = feedItems;
799+
800+
feed.Links.Add(SyndicationLink.CreateSelfLink(
801+
new Uri(Url.PackageAtomFeed(packageRegistration.Id, relativeUrl: false)),
802+
"application/atom+xml"));
803+
804+
feed.Links.Add(SyndicationLink.CreateAlternateLink(
805+
new Uri(Url.Package(packageRegistration.Id, version: null, relativeUrl: false)),
806+
"text/html"));
807+
808+
return new SyndicationAtomActionResult(feed);
809+
}
810+
737811
[HttpGet]
738812
public virtual async Task<ActionResult> License(string id, string version)
739813
{
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System.ServiceModel.Syndication;
2+
using System.Web;
3+
using System.Web.Mvc;
4+
using System.Xml;
5+
6+
namespace NuGetGallery.Infrastructure
7+
{
8+
/// <summary>
9+
/// ActionResult to render an Atom 1.0 feed by using an <see cref="SyndicationFeed"/> instance
10+
/// representing the feed.
11+
/// </summary>
12+
public sealed class SyndicationAtomActionResult : ActionResult
13+
{
14+
public readonly SyndicationFeed SyndicationFeed;
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="SyndicationAtomActionResult"/> class.
18+
/// </summary>
19+
/// <param name="syndicationFeed">The Atom 1.0 <see cref="SyndicationFeed" />.</param>
20+
public SyndicationAtomActionResult(SyndicationFeed syndicationFeed)
21+
{
22+
SyndicationFeed = syndicationFeed;
23+
}
24+
25+
/// <summary>
26+
/// Executes the call to the ActionResult method and returns the created feed to the output response.
27+
/// </summary>
28+
/// <param name="context">The context in which the result is executed. The context information includes the
29+
/// controller, HTTP content, request context, and route data.</param>
30+
public override void ExecuteResult(ControllerContext context)
31+
{
32+
context.HttpContext.Response.ContentType = "application/atom+xml";
33+
Atom10FeedFormatter feedFormatter = new Atom10FeedFormatter(SyndicationFeed);
34+
XmlWriterSettings xmlWriterSettings = new XmlWriterSettings();
35+
36+
if (HttpContext.Current.IsDebuggingEnabled)
37+
{
38+
// Indent the XML for easier viewing but only in Debug mode. In Release mode, everything is output on
39+
// one line for best performance.
40+
xmlWriterSettings.Indent = true;
41+
}
42+
43+
using (XmlWriter xmlWriter = XmlWriter.Create(context.HttpContext.Response.Output, xmlWriterSettings))
44+
{
45+
feedFormatter.WriteTo(xmlWriter);
46+
}
47+
}
48+
}
49+
}

src/NuGetGallery/NuGetGallery.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@
333333
<Compile Include="Infrastructure\Mail\Messages\ReportPackageMessageBase.cs" />
334334
<Compile Include="Infrastructure\Mail\Messages\SigninAssistanceMessage.cs" />
335335
<Compile Include="Infrastructure\Mail\Requests\ReportPackageRequest.cs" />
336+
<Compile Include="Infrastructure\SyndicationAtomActionResult.cs" />
336337
<Compile Include="Migrations\201711082145351_AddAccountDelete.cs" />
337338
<Compile Include="Migrations\201711082145351_AddAccountDelete.Designer.cs">
338339
<DependentUpon>201711082145351_AddAccountDelete.cs</DependentUpon>
@@ -1312,6 +1313,7 @@
13121313
<Content Include="Content\admin\SupportRequestStyles.css" />
13131314
<Content Include="Content\fabric.css" />
13141315
<Content Include="Content\gallery\img\logo-og-600x600.png" />
1316+
<Content Include="Content\gallery\img\rss-24x24.png" />
13151317
<Content Include="Content\Images\icons\apiKey.png" />
13161318
<Content Include="Content\Images\icons\apiKeyExpired.png" />
13171319
<Content Include="Content\Images\icons\apiKeyLegacy.png" />

src/NuGetGallery/RouteName.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public static class RouteName
2626
public const string ApiKeys = "ApiKeys";
2727
public const string Profile = "Profile";
2828
public const string DisplayPackage = "package-route";
29+
public const string DisplayPackageFeed = "package-route-feed";
2930
public const string DownloadPackage = "DownloadPackage";
3031
public const string DownloadSymbolsPackage = "DownloadSymbolsPackage";
3132
public const string DownloadNuGetExe = "DownloadNuGetExe";

src/NuGetGallery/UrlHelperExtensions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,23 @@ public static string PackageDefaultIcon(this UrlHelper url)
238238
+ VirtualPathUtility.ToAbsolute("~/Content/Images/packageDefaultIcon-50x50.png", url.RequestContext.HttpContext.Request.ApplicationPath);
239239
}
240240

241+
public static string PackageAtomFeed(
242+
this UrlHelper url,
243+
string id,
244+
bool relativeUrl = true)
245+
{
246+
string result = GetRouteLink(
247+
url,
248+
RouteName.DisplayPackageFeed,
249+
relativeUrl,
250+
routeValues: new RouteValueDictionary
251+
{
252+
{ "id", id },
253+
});
254+
255+
return result;
256+
}
257+
241258
public static string PackageDownload(
242259
this UrlHelper url,
243260
int feedVersion,

src/NuGetGallery/Views/Packages/DisplayPackage.cshtml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@
8585
<meta property="og:description" content="@Model.Description" />
8686
<meta property="og:determiner" content="a" />
8787
<meta property="og:image" content="@(PackageHelper.ShouldRenderUrl(Model.IconUrl) ? Model.IconUrl : Url.Absolute("~/Content/gallery/img/default-package-icon-256x256.png"))" />
88+
89+
<link rel="alternate" type="application/atom+xml" title="Subscribe to @Model.Id updates" href="@Url.PackageAtomFeed(Model.Id)" />
8890
}
8991

9092
@helper VersionListDivider(int rowCount, bool versionsExpanded)
@@ -860,6 +862,11 @@ foreach (var owner in Model.Owners)
860862
src="@Url.Absolute("~/Content/gallery/img/twitter.svg")"
861863
@ViewHelpers.ImageFallback(Url.Absolute("~/Content/gallery/img/twitter-24x24.png")) />
862864
</a>
865+
<a href="@Url.PackageAtomFeed(Model.Id)" data-track="atom-feed">
866+
<img width="24" height="24" alt="Use the Atom feed to subscribe to new versions of @Model.Id"
867+
src="@Url.Absolute("~/Content/gallery/img/rss.svg")"
868+
@ViewHelpers.ImageFallback(Url.Absolute("~/Content/gallery/img/rss-24x24.png")) />
869+
</a>
863870
</p>
864871
}
865872
</aside>

0 commit comments

Comments
 (0)