Skip to content
This repository was archived by the owner on Dec 21, 2023. It is now read-only.

Commit 2be10d0

Browse files
committed
Add Plugins Preview
1 parent 4bd25c2 commit 2be10d0

9 files changed

Lines changed: 378 additions & 58 deletions

File tree

OpenTabletDriver.Web.Core/OpenTabletDriver.Web.Core.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<ItemGroup Label="NuGet Packages">
99
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
1010
<PackageReference Include="Octokit" Version="0.50.0" />
11+
<PackageReference Include="SharpZipLib" Version="1.3.2" />
1112
</ItemGroup>
1213

1314
</Project>
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Net.Http;
7+
using System.Security.Cryptography;
8+
using System.Text;
9+
using System.Threading.Tasks;
10+
using ICSharpCode.SharpZipLib.GZip;
11+
using ICSharpCode.SharpZipLib.Tar;
12+
using Newtonsoft.Json;
13+
using OpenTabletDriver.Web.Core.Services;
14+
15+
namespace OpenTabletDriver.Web.Core.Plugins
16+
{
17+
public class GitHubPluginMetadataService : IPluginMetadataService
18+
{
19+
public const string REPOSITORY_OWNER = "OpenTabletDriver";
20+
public const string REPOSITORY_NAME = "Plugin-Repository";
21+
22+
public async Task<IEnumerable<PluginMetadata>> GetPlugins()
23+
{
24+
var plugins = await DownloadAsync(REPOSITORY_OWNER, REPOSITORY_NAME);
25+
return plugins.OrderBy(p => p.Name).Distinct(new PluginMetadataComparer());
26+
}
27+
28+
private static HttpClient GetClient()
29+
{
30+
var client = new HttpClient();
31+
client.DefaultRequestHeaders.Add("User-Agent", "OpenTabletDriver-Web");
32+
return client;
33+
}
34+
35+
public static async Task<IEnumerable<PluginMetadata>> DownloadAsync(string owner, string name,
36+
string gitRef = null)
37+
{
38+
string archiveUrl = $"https://api.github.com/repos/{owner}/{name}/tarball/{gitRef}";
39+
return await DownloadAsync(archiveUrl);
40+
}
41+
42+
public static async Task<IEnumerable<PluginMetadata>> DownloadAsync(string archiveUrl)
43+
{
44+
using (var client = GetClient())
45+
using (var httpStream = await client.GetStreamAsync(archiveUrl))
46+
return FromStream(httpStream);
47+
}
48+
49+
public static IEnumerable<PluginMetadata> FromStream(Stream stream)
50+
{
51+
var memStream = new MemoryStream();
52+
stream.CopyTo(memStream);
53+
54+
using (memStream)
55+
using (var gzipStream = new GZipInputStream(memStream))
56+
using (var archive = TarArchive.CreateInputTarArchive(gzipStream, null))
57+
{
58+
string hash = CalculateSHA256(memStream);
59+
string temp = Path.GetTempPath();
60+
61+
Directory.CreateDirectory(temp);
62+
string cacheDir = Path.Join(temp, $"{hash}-OpenTabletDriver-PluginMetadata");
63+
64+
if (!Directory.Exists(cacheDir))
65+
archive.ExtractContents(cacheDir);
66+
67+
return EnumeratePluginMetadata(cacheDir);
68+
}
69+
}
70+
71+
protected static string CalculateSHA256(Stream stream)
72+
{
73+
using (var sha256 = SHA256Managed.Create())
74+
{
75+
var hashData = sha256.ComputeHash(stream);
76+
stream.Position = 0;
77+
var sb = new StringBuilder();
78+
foreach (var val in hashData)
79+
{
80+
var hex = val.ToString("x2");
81+
sb.Append(hex);
82+
}
83+
84+
return sb.ToString();
85+
}
86+
}
87+
88+
protected static IEnumerable<PluginMetadata> EnumeratePluginMetadata(string directoryPath)
89+
{
90+
var serializer = new JsonSerializer();
91+
foreach (var file in Directory.EnumerateFiles(directoryPath, "*.json", SearchOption.AllDirectories))
92+
{
93+
using (var fs = File.OpenRead(file))
94+
using (var sr = new StreamReader(fs))
95+
using (var jr = new JsonTextReader(sr))
96+
yield return serializer.Deserialize<PluginMetadata>(jr);
97+
}
98+
}
99+
100+
private class PluginMetadataComparer : IEqualityComparer<PluginMetadata>
101+
{
102+
public bool Equals(PluginMetadata x, PluginMetadata y)
103+
{
104+
return x.Name == y.Name &&
105+
x.Owner == y.Owner &&
106+
x.RepositoryUrl == y.RepositoryUrl;
107+
}
108+
109+
public int GetHashCode(PluginMetadata obj)
110+
{
111+
return HashCode.Combine(obj.Name, obj.Owner, obj.RepositoryUrl);
112+
}
113+
}
114+
}
115+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System;
2+
3+
namespace OpenTabletDriver.Web.Core.Plugins
4+
{
5+
public class PluginMetadata
6+
{
7+
/// <summary>
8+
/// The name of the plugin.
9+
/// </summary>
10+
public string Name { set; get; }
11+
12+
/// <summary>
13+
/// The owner of the plugin's source code repository.
14+
/// </summary>
15+
public string Owner { set; get; }
16+
17+
/// <summary>
18+
/// The plugin's long description.
19+
/// </summary>
20+
public string Description { set; get; }
21+
22+
/// <summary>
23+
/// The plugins' version.
24+
/// Newer supported versions will be preferred by default.
25+
/// </summary>
26+
public Version PluginVersion { set; get; }
27+
28+
/// <summary>
29+
/// The plugin's minimum supported OpenTabletDriver version,
30+
/// </summary>
31+
public Version SupportedDriverVersion { set; get; }
32+
33+
/// <summary>
34+
/// The plugin's source code repository URL.
35+
/// </summary>
36+
public string RepositoryUrl { set; get; }
37+
38+
/// <summary>
39+
/// The plugin's binary download URL.
40+
/// </summary>
41+
public string DownloadUrl { set; get; }
42+
43+
/// <summary>
44+
/// The compression format used in the binary download from <see cref="DownloadUrl"/>.
45+
/// </summary>
46+
public string CompressionFormat { set; get; }
47+
48+
/// <summary>
49+
/// The SHA256 hash of the file at <see cref="DownloadUrl"/>, used for verifying file integrity.
50+
/// </summary>
51+
public string SHA256 { set; get; }
52+
53+
/// <summary>
54+
/// The plugin's wiki URL.
55+
/// </summary>
56+
public string WikiUrl { set; get; }
57+
58+
/// <summary>
59+
/// The SPDX license identifier expression.
60+
/// </summary>
61+
public string LicenseIdentifier { set; get; }
62+
63+
public static bool Match(PluginMetadata primary, PluginMetadata secondary)
64+
{
65+
if (primary == null || secondary == null)
66+
return false;
67+
68+
return primary.Name == secondary.Name &&
69+
primary.Owner == secondary.Owner &&
70+
primary.RepositoryUrl == secondary.RepositoryUrl;
71+
}
72+
}
73+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.Collections.Generic;
2+
using System.Threading.Tasks;
3+
using OpenTabletDriver.Web.Core.Plugins;
4+
5+
namespace OpenTabletDriver.Web.Core.Services
6+
{
7+
public interface IPluginMetadataService
8+
{
9+
Task<IEnumerable<PluginMetadata>> GetPlugins();
10+
}
11+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Threading.Tasks;
2+
using Microsoft.AspNetCore.Mvc;
3+
using OpenTabletDriver.Web.Core.Services;
4+
5+
namespace OpenTabletDriver.Web.Controllers
6+
{
7+
public class PluginsController : Controller
8+
{
9+
public PluginsController(IPluginMetadataService pluginMetadataService)
10+
{
11+
this.pluginMetadataService = pluginMetadataService;
12+
}
13+
14+
private IPluginMetadataService pluginMetadataService;
15+
16+
public async Task<IActionResult> Index()
17+
{
18+
var metadata = await pluginMetadataService.GetPlugins();
19+
return View(metadata);
20+
}
21+
}
22+
}

OpenTabletDriver.Web/Startup.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using OpenTabletDriver.Web.Controllers;
99
using OpenTabletDriver.Web.Core.Framework;
1010
using OpenTabletDriver.Web.Core.GitHub.Services;
11+
using OpenTabletDriver.Web.Core.Plugins;
1112
using OpenTabletDriver.Web.Core.Services;
1213

1314
namespace OpenTabletDriver.Web
@@ -27,6 +28,7 @@ public void ConfigureServices(IServiceCollection services)
2728
services.AddControllersWithViews();
2829
services.Add(new ServiceDescriptor(typeof(IReleaseService), new GitHubReleaseService()));
2930
services.Add(new ServiceDescriptor(typeof(IFrameworkService), new DotnetCoreService()));
31+
services.Add(new ServiceDescriptor(typeof(IPluginMetadataService), new GitHubPluginMetadataService()));
3032
services.Configure<ForwardedHeadersOptions>(options =>
3133
{
3234
options.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
@using OpenTabletDriver.Web.Core.Plugins
2+
@using System.Data.Common
3+
4+
@model IEnumerable<PluginMetadata>
5+
6+
@{
7+
ViewBag.Title = "Plugins";
8+
var pluginVersions = Model.Select(m => m.SupportedDriverVersion)
9+
.Distinct()
10+
.OrderByDescending(v => v.ToString());
11+
}
12+
13+
<div class="container">
14+
<div class="d-flex mb-3">
15+
<div class="flex-fill">
16+
<div class="form-floating me-3">
17+
<input type="text" class="form-control" id="searchInput" placeholder="Search" onkeyup="filter()"/>
18+
<label for="searchInput">Search</label>
19+
</div>
20+
</div>
21+
<div class="flex-fill">
22+
<div class="form-floating">
23+
<select class="form-select" id="pluginVersionSelector" onchange="filter()">
24+
<option value="">All Versions</option>
25+
@foreach (var version in pluginVersions)
26+
{
27+
<option value="@version">@version</option>
28+
}
29+
</select>
30+
<label class="form-label" for="pluginVersionSelector">Driver Version</label>
31+
</div>
32+
</div>
33+
</div>
34+
35+
<div id="metadataList">
36+
@foreach (var metadata in Model)
37+
{
38+
<div class="card mb-2 plugin-metadata-card"
39+
data-name="@metadata.Name" data-version="@metadata.SupportedDriverVersion">
40+
<div class="card-body d-flex flex-sm-row flex-column">
41+
<div class="flex-sm-fill p-2">
42+
<span class="h4">@metadata.Name</span>
43+
<span class="text-sm text-muted"> - @metadata.Owner - @metadata.PluginVersion</span><br/>
44+
<div class="mt-2 ms-2">
45+
@metadata.Description
46+
</div>
47+
</div>
48+
<div class="mt-auto text-center">
49+
<button type="button" class="btn-sm btn-primary me-1"
50+
@(string.IsNullOrWhiteSpace(metadata.DownloadUrl) ? "disabled" : string.Empty)
51+
onclick="location.href='@metadata.DownloadUrl'">
52+
Download
53+
</button>
54+
<button type="button" class="btn-sm btn-info me-1"
55+
@(string.IsNullOrWhiteSpace(metadata.RepositoryUrl) ? "disabled" : string.Empty)
56+
onclick="location.href='@metadata.WikiUrl'">
57+
Wiki
58+
</button>
59+
<button type="button" class="btn-sm btn-info"
60+
@(string.IsNullOrWhiteSpace(metadata.RepositoryUrl) ? "disabled" : string.Empty)
61+
onclick="location.href='@metadata.RepositoryUrl'">
62+
Source
63+
</button>
64+
</div>
65+
</div>
66+
</div>
67+
}
68+
</div>
69+
</div>
70+
71+
@section Scripts
72+
{
73+
<script type="text/javascript">
74+
filter();
75+
76+
function filter() {
77+
let selector = document.getElementById('pluginVersionSelector');
78+
let targetVersion = selector.value;
79+
80+
let searchInput = document.getElementById('searchInput');
81+
let searchFilter = searchInput.value.toUpperCase();
82+
83+
let container = document.getElementById('metadataList');
84+
let cards = container.getElementsByClassName('plugin-metadata-card');
85+
86+
for (let i = 0; i < cards.length; i++) {
87+
let card = cards[i];
88+
let name = card.getAttribute('data-name').toUpperCase();
89+
let version = card.getAttribute('data-version');
90+
91+
let searchMatches = name.indexOf(searchFilter) >= 0;
92+
let versionMatches = targetVersion == '' || targetVersion == version;
93+
94+
card.style.display = searchMatches && versionMatches ? '' : 'none';
95+
}
96+
}
97+
</script>
98+
}

0 commit comments

Comments
 (0)