main
cs 309 lines 12.3 KB
Raw
1 // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3 namespace WixToolset.Core.ExtensionCache
4 {
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using NuGet.Common;
12 using NuGet.Configuration;
13 using NuGet.Credentials;
14 using NuGet.Packaging;
15 using NuGet.Protocol;
16 using NuGet.Protocol.Core.Types;
17 using NuGet.Versioning;
18 using WixToolset.Extensibility.Data;
19 using WixToolset.Extensibility.Services;
20
21 /// <summary>
22 /// Extension cache manager.
23 /// </summary>
24 internal class ExtensionCacheManager
25 {
26 private IReadOnlyCollection<IExtensionCacheLocation> cacheLocations;
27
28 public ExtensionCacheManager(IMessaging messaging, IExtensionManager extensionManager)
29 {
30 this.Messaging = messaging;
31 this.ExtensionManager = extensionManager;
32
33 this.WixVersion = typeof(ExtensionCacheManager).Assembly.GetName().Version.Major.ToString();
34 }
35
36 private IMessaging Messaging { get; }
37
38 private IExtensionManager ExtensionManager { get; }
39
40 public string WixVersion { get; }
41
42 public async Task<bool> AddAsync(bool global, string extension, CancellationToken cancellationToken)
43 {
44 if (String.IsNullOrEmpty(extension))
45 {
46 throw new ArgumentNullException(nameof(extension));
47 }
48
49 (var extensionId, var extensionVersion) = ParseExtensionReference(extension);
50
51 var result = await this.DownloadAndExtractAsync(global, extensionId, extensionVersion, cancellationToken);
52
53 return result;
54 }
55
56 public Task<bool> RemoveAsync(bool global, string extension, CancellationToken cancellationToken)
57 {
58 if (String.IsNullOrEmpty(extension))
59 {
60 throw new ArgumentNullException(nameof(extension));
61 }
62
63 (var extensionId, var extensionVersion) = ParseExtensionReference(extension);
64
65 var cacheFolder = this.GetCacheFolder(global);
66
67 var extensionFolder = Path.Combine(cacheFolder, extensionId, extensionVersion);
68
69 if (Directory.Exists(extensionFolder))
70 {
71 cancellationToken.ThrowIfCancellationRequested();
72
73 Directory.Delete(cacheFolder, true);
74 return Task.FromResult(true);
75 }
76
77 return Task.FromResult(false);
78 }
79
80 public Task<IEnumerable<CachedExtension>> ListAsync(bool global, string extension, CancellationToken cancellationToken)
81 {
82 var found = new List<CachedExtension>();
83
84 (var extensionId, var extensionVersion) = ParseExtensionReference(extension);
85
86 var cacheFolders = this.GetCacheFolders(global);
87
88 foreach (var cacheFolder in cacheFolders)
89 {
90 var searchFolder = Path.Combine(cacheFolder, extensionId, extensionVersion);
91
92 if (!Directory.Exists(searchFolder))
93 {
94 }
95 else if (!String.IsNullOrEmpty(extensionVersion)) // looking for an explicit version of an extension.
96 {
97 var present = this.ExtensionFileExists(cacheFolder, extensionId, extensionVersion);
98 found.Add(new CachedExtension(extensionId, extensionVersion, !present));
99 }
100 else // looking for all versions of an extension or all versions of all extensions.
101 {
102 IEnumerable<string> foundExtensionIds;
103
104 if (String.IsNullOrEmpty(extensionId))
105 {
106 // Looking for all versions of all extensions.
107 foundExtensionIds = Directory.GetDirectories(cacheFolder).Select(folder => Path.GetFileName(folder)).ToList();
108 }
109 else
110 {
111 // Looking for all versions of a single extension.
112 var extensionFolder = Path.Combine(cacheFolder, extensionId);
113 foundExtensionIds = Directory.Exists(extensionFolder) ? new[] { extensionId } : Array.Empty<string>();
114 }
115
116 foreach (var foundExtensionId in foundExtensionIds)
117 {
118 var extensionFolder = Path.Combine(cacheFolder, foundExtensionId);
119
120 foreach (var foundExtensionVersionFolder in Directory.GetDirectories(extensionFolder))
121 {
122 cancellationToken.ThrowIfCancellationRequested();
123
124 var foundExtensionVersion = Path.GetFileName(foundExtensionVersionFolder);
125
126 if (!NuGetVersion.TryParse(foundExtensionVersion, out _))
127 {
128 continue;
129 }
130
131 var present = this.ExtensionFileExists(cacheFolder, foundExtensionId, foundExtensionVersion);
132 found.Add(new CachedExtension(foundExtensionId, foundExtensionVersion, !present));
133 }
134 }
135 }
136 }
137
138 return Task.FromResult((IEnumerable<CachedExtension>)found);
139 }
140
141 private string GetCacheFolder(bool global)
142 {
143 if (this.cacheLocations == null)
144 {
145 this.cacheLocations = this.ExtensionManager.GetCacheLocations();
146 }
147
148 var requestedScope = global ? ExtensionCacheLocationScope.User : ExtensionCacheLocationScope.Project;
149
150 var cacheLocation = this.cacheLocations.First(l => l.Scope == requestedScope);
151
152 return cacheLocation.Path;
153 }
154
155 private IEnumerable<string> GetCacheFolders(bool global)
156 {
157 if (this.cacheLocations == null)
158 {
159 this.cacheLocations = this.ExtensionManager.GetCacheLocations();
160 }
161
162 var cacheLocations = this.cacheLocations.Where(l => global || l.Scope == ExtensionCacheLocationScope.Project).OrderBy(l => l.Scope).Select(l => l.Path);
163
164 return cacheLocations;
165 }
166
167 private async Task<bool> DownloadAndExtractAsync(bool global, string id, string version, CancellationToken cancellationToken)
168 {
169 var logger = NullLogger.Instance;
170
171 DefaultCredentialServiceUtility.SetupDefaultCredentialService(logger, nonInteractive: false);
172
173 var settings = Settings.LoadDefaultSettings(root: Environment.CurrentDirectory);
174 var sources = PackageSourceProvider.LoadPackageSources(settings).Where(s => s.IsEnabled);
175
176 using (var cache = new SourceCacheContext())
177 {
178 PackageSource versionSource = null;
179
180 var nugetVersion = String.IsNullOrEmpty(version) ? null : new NuGetVersion(version);
181
182 if (nugetVersion is null)
183 {
184 foreach (var source in sources)
185 {
186 var repository = Repository.Factory.GetCoreV3(source.Source);
187 var resource = await repository.GetResourceAsync<FindPackageByIdResource>();
188
189 try
190 {
191 var availableVersions = await resource.GetAllVersionsAsync(id, cache, logger, cancellationToken);
192 foreach (var availableVersion in availableVersions)
193 {
194 if (nugetVersion is null || nugetVersion < availableVersion)
195 {
196 nugetVersion = availableVersion;
197 versionSource = source;
198 }
199 }
200 }
201 catch (FatalProtocolException e)
202 {
203 this.Messaging.Write(ExtensionCacheWarnings.NugetException(id, e.Message));
204 }
205 }
206
207 if (nugetVersion is null)
208 {
209 return false;
210 }
211 }
212
213 var searchSources = versionSource is null ? sources : new[] { versionSource };
214
215 var cacheFolder = this.GetCacheFolder(global);
216
217 var extensionFolder = Path.Combine(cacheFolder, id, nugetVersion.ToString());
218
219 var extensionPackageRootFolderName = this.ExtensionManager.GetExtensionPackageRootFolderName();
220
221 foreach (var source in searchSources)
222 {
223 var repository = Repository.Factory.GetCoreV3(source.Source);
224 var resource = await repository.GetResourceAsync<FindPackageByIdResource>();
225
226 using (var stream = new MemoryStream())
227 {
228 var downloaded = await resource.CopyNupkgToStreamAsync(id, nugetVersion, stream, cache, logger, cancellationToken);
229
230 if (downloaded)
231 {
232 stream.Position = 0;
233
234 using (var archive = new PackageArchiveReader(stream))
235 {
236 var files = archive.GetFiles(extensionPackageRootFolderName);
237 if (!files.Any())
238 {
239 this.Messaging.Write(ExtensionCacheWarnings.MissingExtensionPackageRootFolder(id, nugetVersion.ToString(), extensionPackageRootFolderName, this.WixVersion));
240 return false;
241 }
242
243 Directory.CreateDirectory(extensionFolder);
244 await archive.CopyFilesAsync(extensionFolder, files, this.ExtractProgress, logger, cancellationToken);
245 }
246
247 return true;
248 }
249 }
250 }
251 }
252
253 return false;
254 }
255
256 private string ExtractProgress(string sourceFile, string targetPath, Stream fileStream)
257 {
258 return fileStream.CopyToFile(targetPath);
259 }
260
261 private static (string extensionId, string extensionVersion) ParseExtensionReference(string extensionReference)
262 {
263 var extensionId = extensionReference ?? String.Empty;
264 var extensionVersion = String.Empty;
265
266 var index = extensionId.LastIndexOf('/');
267 if (index > 0)
268 {
269 extensionVersion = extensionReference.Substring(index + 1);
270 extensionId = extensionReference.Substring(0, index);
271
272 if (!NuGetVersion.TryParse(extensionVersion, out _))
273 {
274 throw new ArgumentException($"Invalid extension version in {extensionReference}");
275 }
276
277 if (String.IsNullOrEmpty(extensionId))
278 {
279 throw new ArgumentException($"Invalid extension id in {extensionReference}");
280 }
281 }
282
283 return (extensionId, extensionVersion);
284 }
285
286 private bool ExtensionFileExists(string baseFolder, string extensionId, string extensionVersion)
287 {
288 var packageRootFolderName = this.ExtensionManager.GetExtensionPackageRootFolderName();
289
290 var extensionFolder = Path.Combine(baseFolder, extensionId, extensionVersion, packageRootFolderName);
291 if (!Directory.Exists(extensionFolder))
292 {
293 this.Messaging.Write(ExtensionCacheWarnings.MissingExtensionPackageRootFolder(extensionId, extensionVersion, packageRootFolderName, this.WixVersion));
294 return false;
295 }
296
297 var extensionAssembly = Path.Combine(extensionFolder, extensionId + ".dll");
298
299 var present = File.Exists(extensionAssembly);
300 if (!present)
301 {
302 extensionAssembly = Path.Combine(extensionFolder, extensionId + ".exe");
303 present = File.Exists(extensionAssembly);
304 }
305
306 return present;
307 }
308 }
309 }