Skip to content

Commit 990e0fe

Browse files
committed
Add a command to GalleryTools for batch reserving namespaces
1 parent 6fd3bae commit 990e0fe

3 files changed

Lines changed: 243 additions & 0 deletions

File tree

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Threading.Tasks;
9+
using Autofac;
10+
using Microsoft.Extensions.CommandLineUtils;
11+
using NuGet.Packaging;
12+
using NuGet.Packaging.Core;
13+
using NuGet.Services.Entities;
14+
using NuGet.Versioning;
15+
using NuGetGallery;
16+
17+
namespace GalleryTools.Commands
18+
{
19+
public static class ReserveNamespacesCommand
20+
{
21+
private const int DefaultSleep = 0;
22+
private const string PathOption = "--path";
23+
24+
public static void Configure(CommandLineApplication config)
25+
{
26+
config.Description = "Bulk reserve namespaces";
27+
config.HelpOption("-? | -h | --help");
28+
29+
var pathOptions = config.Option(
30+
$"-p | {PathOption}",
31+
"A path to a simple text file, which is the list of namespaces to reserve. One namespace per line. Prefix namespaces should end with '*', otherwise they will be exact match reservations.",
32+
CommandOptionType.SingleValue);
33+
34+
var sleepDurationOption = config.Option(
35+
"-s | --sleep",
36+
$"The duration in seconds to sleep between each reservation (default: 0).",
37+
CommandOptionType.SingleValue);
38+
39+
config.OnExecute(() =>
40+
{
41+
return ExecuteAsync(pathOptions, sleepDurationOption).GetAwaiter().GetResult();
42+
});
43+
}
44+
45+
private static async Task<int> ExecuteAsync(
46+
CommandOption pathOption,
47+
CommandOption sleepDurationOption)
48+
{
49+
if (!pathOption.HasValue())
50+
{
51+
Console.WriteLine($"The '{PathOption}' parameter is required.");
52+
return 1;
53+
}
54+
55+
var sleepDuration = TimeSpan.FromSeconds(DefaultSleep);
56+
if (sleepDurationOption.HasValue())
57+
{
58+
sleepDuration = TimeSpan.FromSeconds(int.Parse(sleepDurationOption.Value()));
59+
60+
if (sleepDuration < TimeSpan.Zero)
61+
{
62+
Console.WriteLine("The sleep duration must be zero or more seconds.");
63+
return 1;
64+
}
65+
}
66+
67+
var path = pathOption.Value();
68+
var completedPath = path + ".progress";
69+
var remainingList = GetRemainingList(path, completedPath);
70+
if (!remainingList.Any())
71+
{
72+
Console.WriteLine("No namespaces were found to reserve.");
73+
return 1;
74+
}
75+
76+
var builder = new ContainerBuilder();
77+
builder.RegisterType<ReservedNamespaceService>().AsSelf();
78+
builder.RegisterAssemblyModules(typeof(DefaultDependenciesModule).Assembly);
79+
var container = builder.Build();
80+
var service = container.Resolve<ReservedNamespaceService>();
81+
82+
var totalCounter = 0;
83+
foreach (var reservedNamespace in remainingList)
84+
{
85+
Console.Write($"Reserving '{reservedNamespace.Value}' IsPrefix = {reservedNamespace.IsPrefix}...");
86+
try
87+
{
88+
var matching = service
89+
.FindReservedNamespacesForPrefixList(new[] { reservedNamespace.Value })
90+
.SingleOrDefault(x => ReservedNamespaceComparer.Instance.Equals(x, reservedNamespace));
91+
if (matching != null)
92+
{
93+
Console.WriteLine(" already exists.");
94+
AppendReservedNamespace(completedPath, reservedNamespace);
95+
continue;
96+
}
97+
98+
await service.AddReservedNamespaceAsync(reservedNamespace);
99+
AppendReservedNamespace(completedPath, reservedNamespace);
100+
totalCounter++;
101+
Console.WriteLine(" done.");
102+
}
103+
catch (Exception e)
104+
{
105+
Console.WriteLine(" error!");
106+
Console.WriteLine(e);
107+
}
108+
109+
if (sleepDuration > TimeSpan.Zero)
110+
{
111+
Console.WriteLine($"Sleeping for {sleepDuration}.");
112+
await Task.Delay(sleepDuration);
113+
}
114+
}
115+
116+
Console.WriteLine($"All done. Added {totalCounter} reserved namespace(s).");
117+
118+
return 0;
119+
}
120+
121+
private static List<ReservedNamespace> GetRemainingList(string path, string completedPath)
122+
{
123+
Console.WriteLine($"Reading reserved namespaces from {path}...");
124+
var all = ReadReservedNamespaces(path);
125+
126+
var completed = new List<ReservedNamespace>();
127+
if (File.Exists(completedPath))
128+
{
129+
Console.WriteLine($"Reading completed reserved namespaces from {completedPath}...");
130+
completed.AddRange(ReadReservedNamespaces(completedPath));
131+
}
132+
133+
var remaining = all.Except(completed, ReservedNamespaceComparer.Instance).ToList();
134+
if (remaining.Count != all.Count)
135+
{
136+
Console.WriteLine($"{all.Count - remaining.Count} reserved namespaces(s) are already done.");
137+
}
138+
Console.WriteLine($"{remaining.Count} reserved namespaces(s) to add.");
139+
140+
return remaining;
141+
}
142+
143+
private static void AppendReservedNamespace(string completedListPath, ReservedNamespace reservedNamespace)
144+
{
145+
using (var fileStream = new FileStream(completedListPath, FileMode.Append, FileAccess.Write))
146+
using (var writer = new StreamWriter(fileStream))
147+
{
148+
writer.WriteLine($"{reservedNamespace.Value}{(reservedNamespace.IsPrefix ? "*" : string.Empty)}");
149+
}
150+
}
151+
152+
private static List<ReservedNamespace> ReadReservedNamespaces(string path)
153+
{
154+
var uniqueReservedNamespaces = new HashSet<ReservedNamespace>(new ReservedNamespaceComparer());
155+
var output = new List<ReservedNamespace>();
156+
int lineNumber = 0;
157+
string line;
158+
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read))
159+
using (var reader = new StreamReader(fileStream))
160+
{
161+
while ((line = reader.ReadLine()) != null)
162+
{
163+
lineNumber++;
164+
165+
if (string.IsNullOrWhiteSpace(line))
166+
{
167+
continue;
168+
}
169+
170+
var value = line.Trim();
171+
var isPrefix = false;
172+
if (line.EndsWith("*"))
173+
{
174+
value = value.Substring(0, value.Length - 1);
175+
isPrefix = true;
176+
}
177+
178+
// Ensure the reserved namespace is actually a valid package ID.
179+
var validatedPrefix = value;
180+
if (isPrefix)
181+
{
182+
// Prefix reserved namespaces can end with '-' and '.'. Package IDs cannot.
183+
if (value.EndsWith("-") || value.EndsWith("."))
184+
{
185+
validatedPrefix = validatedPrefix.Substring(0, validatedPrefix.Length - 1);
186+
}
187+
}
188+
189+
if (!PackageIdValidator.IsValidPackageId(validatedPrefix))
190+
{
191+
Console.WriteLine($"Line {lineNumber}: Ignoring invalid reserved namespace (validated: '{validatedPrefix}', original: '{line}').");
192+
continue;
193+
}
194+
195+
var reservedNamespace = new ReservedNamespace
196+
{
197+
Value = value,
198+
IsPrefix = isPrefix,
199+
IsSharedNamespace = false,
200+
};
201+
202+
if (!uniqueReservedNamespaces.Add(reservedNamespace))
203+
{
204+
Console.WriteLine($"Line {lineNumber}: Ignoring duplicate reserved namespace.");
205+
continue;
206+
}
207+
208+
output.Add(reservedNamespace);
209+
}
210+
}
211+
212+
return output;
213+
}
214+
215+
private class ReservedNamespaceComparer : IEqualityComparer<ReservedNamespace>
216+
{
217+
public static ReservedNamespaceComparer Instance { get; } = new ReservedNamespaceComparer();
218+
219+
public bool Equals(ReservedNamespace x, ReservedNamespace y)
220+
{
221+
if (ReferenceEquals(x, y))
222+
{
223+
return true;
224+
}
225+
226+
if (x is null || y is null)
227+
{
228+
return false;
229+
}
230+
231+
return StringComparer.OrdinalIgnoreCase.Equals(x.Value, y.Value)
232+
&& x.IsPrefix == y.IsPrefix;
233+
}
234+
235+
public int GetHashCode(ReservedNamespace obj)
236+
{
237+
return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Value) ^ obj.IsPrefix.GetHashCode();
238+
}
239+
}
240+
}
241+
}

src/GalleryTools/GalleryTools.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
<Compile Include="Commands\BackfillRepositoryMetadataCommand.cs" />
5050
<Compile Include="Commands\HashCommand.cs" />
5151
<Compile Include="Commands\ApplyTenantPolicyCommand.cs" />
52+
<Compile Include="Commands\ReserveNamespacesCommand.cs" />
5253
<Compile Include="Commands\ReflowCommand.cs" />
5354
<Compile Include="Commands\UpdateIsLatestCommand.cs" />
5455
<Compile Include="Commands\VerifyApiKeyCommand.cs" />

src/GalleryTools/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public static int Main(params string[] args)
2121
commandLineApplication.Command("filldevdeps", BackfillDevelopmentDependencyCommand.Configure);
2222
commandLineApplication.Command("verifyapikey", VerifyApiKeyCommand.Configure);
2323
commandLineApplication.Command("updateIsLatest", UpdateIsLatestCommand.Configure);
24+
commandLineApplication.Command("reservenamespaces", ReserveNamespacesCommand.Configure);
2425

2526
try
2627
{

0 commit comments

Comments
 (0)