/
BrowserFetcher.cs
executable file
·306 lines (272 loc) · 11.3 KB
/
BrowserFetcher.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
using System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Net;
using System.IO.Compression;
namespace PuppeteerSharp
{
/// <summary>
/// BrowserFetcher can download and manage different versions of Chromium.
/// BrowserFetcher operates on revision strings that specify a precise version of Chromium, e.g. 533271. Revision strings can be obtained from omahaproxy.appspot.com.
/// </summary>
/// <example>
/// Example on how to use BrowserFetcher to download a specific version of Chromium and run Puppeteer against it:
/// <code>
/// var browserFetcher = Puppeteer.CreateBrowserFetcher();
/// var revisionInfo = await browserFetcher.DownloadAsync(533271);
/// var browser = await await Puppeteer.LaunchAsync(new LaunchOptions { ExecutablePath = revisionInfo.ExecutablePath});
/// </code>
/// </example>
public class BrowserFetcher
{
private const string DefaultDownloadHost = "https://storage.googleapis.com";
private static readonly Dictionary<Platform, string> _downloadUrls = new Dictionary<Platform, string> {
{Platform.Linux, "{0}/chromium-browser-snapshots/Linux_x64/{1}/{2}.zip"},
{Platform.MacOS, "{0}/chromium-browser-snapshots/Mac/{1}/{2}.zip"},
{Platform.Win32, "{0}/chromium-browser-snapshots/Win/{1}/{2}.zip"},
{Platform.Win64, "{0}/chromium-browser-snapshots/Win_x64/{1}/{2}.zip"}
};
/// <summary>
/// Default chromiumg revision.
/// </summary>
public const int DefaultRevision = 590951;
/// <summary>
/// Gets the downloads folder.
/// </summary>
/// <value>The downloads folder.</value>
public string DownloadsFolder { get; }
/// <summary>
/// A download host to be used. Defaults to https://storage.googleapis.com.
/// </summary>
/// <value>The download host.</value>
public string DownloadHost { get; }
/// <summary>
/// Gets the platform.
/// </summary>
/// <value>The platform.</value>
public Platform Platform { get; }
/// <summary>
/// Occurs when download progress in <see cref="DownloadAsync(int)"/> changes.
/// </summary>
public event DownloadProgressChangedEventHandler DownloadProgressChanged;
/// <summary>
/// Initializes a new instance of the <see cref="BrowserFetcher"/> class.
/// </summary>
public BrowserFetcher()
{
DownloadsFolder = Path.Combine(Directory.GetCurrentDirectory(), ".local-chromium");
DownloadHost = DefaultDownloadHost;
Platform = GetCurrentPlatform();
}
/// <summary>
/// Initializes a new instance of the <see cref="BrowserFetcher"/> class.
/// </summary>
/// <param name="options">Fetch options.</param>
public BrowserFetcher(BrowserFetcherOptions options)
{
DownloadsFolder = string.IsNullOrEmpty(options.Path) ?
Path.Combine(Directory.GetCurrentDirectory(), ".local-chromium") :
options.Path;
DownloadHost = string.IsNullOrEmpty(options.Host) ? DefaultDownloadHost : options.Host;
Platform = options.Platform ?? GetCurrentPlatform();
}
#region Public Methods
/// <summary>
/// The method initiates a HEAD request to check if the revision is available.
/// </summary>
/// <returns>Whether the version is available or not.</returns>
/// <param name="revision">A revision to check availability.</param>
public async Task<bool> CanDownloadAsync(int revision)
{
var url = GetDownloadURL(Platform, DownloadHost, revision);
var client = new HttpClient();
var response = await client.SendAsync(new HttpRequestMessage
{
RequestUri = new Uri(url),
Method = HttpMethod.Head
}).ConfigureAwait(false);
return response.IsSuccessStatusCode;
}
/// <summary>
/// A list of all revisions available locally on disk.
/// </summary>
/// <returns>The available revisions.</returns>
public IEnumerable<int> LocalRevisions()
{
var directoryInfo = new DirectoryInfo(DownloadsFolder);
if (directoryInfo.Exists)
{
return directoryInfo.GetDirectories().Select(d => GetRevisionFromPath(d.Name)).Where(v => v > 0);
}
return new int[] { };
}
/// <summary>
/// Removes a downloaded revision.
/// </summary>
/// <param name="revision">Revision to remove.</param>
public void Remove(int revision)
{
var directory = new DirectoryInfo(GetFolderPath(revision));
if (directory.Exists)
{
directory.Delete(true);
}
}
/// <summary>
/// Gets the revision info.
/// </summary>
/// <returns>Revision info.</returns>
/// <param name="revision">A revision to get info for.</param>
public RevisionInfo RevisionInfo(int revision)
{
var result = new RevisionInfo
{
FolderPath = GetFolderPath(revision),
Url = GetDownloadURL(Platform, DownloadHost, revision),
Revision = revision,
Platform = Platform
};
result.ExecutablePath = GetExecutablePath(Platform, revision, result.FolderPath);
result.Local = new DirectoryInfo(result.FolderPath).Exists;
return result;
}
/// <summary>
/// Downloads the revision.
/// </summary>
/// <returns>Task which resolves to the completed download.</returns>
/// <param name="revision">Revision.</param>
public async Task<RevisionInfo> DownloadAsync(int revision)
{
var url = GetDownloadURL(Platform, DownloadHost, revision);
var zipPath = Path.Combine(DownloadsFolder, $"download-{Platform.ToString()}-{revision}.zip");
var folderPath = GetFolderPath(revision);
if (new DirectoryInfo(folderPath).Exists)
{
return RevisionInfo(revision);
}
var downloadFolder = new DirectoryInfo(DownloadsFolder);
if (!downloadFolder.Exists)
{
downloadFolder.Create();
}
var webClient = new WebClient();
if (DownloadProgressChanged != null)
{
webClient.DownloadProgressChanged += DownloadProgressChanged;
}
await webClient.DownloadFileTaskAsync(new Uri(url), zipPath).ConfigureAwait(false);
if (Platform == Platform.MacOS)
{
//ZipFile and many others unzip libraries have issues extracting .app files
//Until we have a clear solution we'll call the native unzip tool
//https://github.com/dotnet/corefx/issues/15516
NativeExtractToDirectory(zipPath, folderPath);
}
else
{
ZipFile.ExtractToDirectory(zipPath, folderPath);
}
new FileInfo(zipPath).Delete();
return RevisionInfo(revision);
}
/// <summary>
/// Gets the executable path for a revision.
/// </summary>
/// <returns>The executable path.</returns>
/// <param name="revision">Revision.</param>
public string GetExecutablePath(int revision)
=> GetExecutablePath(Platform, revision, GetFolderPath(revision));
/// <summary>
/// Gets the executable path.
/// </summary>
/// <returns>The executable path.</returns>
/// <param name="platform">Platform.</param>
/// <param name="revision">Revision.</param>
/// <param name="folderPath">Folder path.</param>
public static string GetExecutablePath(Platform platform, int revision, string folderPath)
{
switch (platform)
{
case Platform.MacOS:
return Path.Combine(folderPath, GetArchiveName(platform, revision), "Chromium.app", "Contents",
"MacOS", "Chromium");
case Platform.Linux:
return Path.Combine(folderPath, GetArchiveName(platform, revision), "chrome");
case Platform.Win32:
case Platform.Win64:
return Path.Combine(folderPath, GetArchiveName(platform, revision), "chrome.exe");
default:
throw new ArgumentException("Invalid platform", nameof(platform));
}
}
#endregion
#region Private Methods
internal static Platform GetCurrentPlatform()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return Platform.MacOS;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return Platform.Linux;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return RuntimeInformation.OSArchitecture == Architecture.X64 ? Platform.Win64 : Platform.Win32;
}
return Platform.Unknown;
}
private string GetFolderPath(int revision)
=> Path.Combine(DownloadsFolder, $"{Platform.ToString()}-{revision}");
private void NativeExtractToDirectory(string zipPath, string folderPath)
{
var process = new Process();
process.StartInfo.FileName = "unzip";
process.StartInfo.Arguments = $"{zipPath} -d {folderPath}";
process.Start();
process.WaitForExit();
}
private int GetRevisionFromPath(string folderName)
{
var splits = folderName.Split('-');
if (splits.Length != 2)
{
return 0;
}
if (!Enum.TryParse<Platform>(splits[0], out var platform))
{
platform = Platform.Unknown;
}
if (!_downloadUrls.Keys.Contains(platform))
{
return 0;
}
int.TryParse(splits[1], out var revision);
return revision;
}
private static string GetArchiveName(Platform platform, int revision)
{
switch (platform)
{
case Platform.Linux:
return "chrome-linux";
case Platform.MacOS:
return "chrome-mac";
case Platform.Win32:
case Platform.Win64:
return revision > 591479 ? "chrome-win" : "chrome-win32";
default:
throw new ArgumentException("Invalid platform", nameof(platform));
}
}
private static string GetDownloadURL(Platform platform, string host, int revision)
=> string.Format(_downloadUrls[platform], host, revision, GetArchiveName(platform, revision));
#endregion
}
}