Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement SignalR as middleware for project export enhancement #817

Merged
merged 47 commits into from
Nov 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
f4401fb
Rudimentary SignalR implementation
imnasnainaec Nov 11, 2020
6babbdd
Custom middleware
imnasnainaec Nov 12, 2020
7326b3b
Merge in 'maser'
imnasnainaec Nov 17, 2020
5e6baf6
fmt
imnasnainaec Nov 17, 2020
f94ade5
Intercept
imnasnainaec Nov 17, 2020
2102ccd
Add to AppBar a DownloadButton that is dynamically tied to exportStat…
imnasnainaec Nov 17, 2020
f250e2f
Merge branch 'master' into signalr
imnasnainaec Nov 18, 2020
583c9e3
Make ProjectExport move out; move download action into DownloadButton…
imnasnainaec Nov 18, 2020
686f922
finish export/download split; extract signalR as own component in fro…
imnasnainaec Nov 19, 2020
064cd99
Merge in 'master'
imnasnainaec Nov 20, 2020
6444ee3
Split download and delete/reset
imnasnainaec Nov 20, 2020
24d3b46
Merge in 'master'
imnasnainaec Nov 20, 2020
e55fdf4
Fix useEffect for turning on SignalR connection
imnasnainaec Nov 20, 2020
d4ee912
Add export reset to reducer
imnasnainaec Nov 20, 2020
43e6b1b
Improve connection logging
imnasnainaec Nov 20, 2020
39cf3e0
Remove dead code
johnthagen Nov 20, 2020
86158ed
Wrap long line
johnthagen Nov 20, 2020
84bd131
Rename CombineHub.cs
johnthagen Nov 20, 2020
1d5d455
Mock out HubContext in Lift tests
johnthagen Nov 20, 2020
cc997b4
Merge branch 'master' into signalr
johnthagen Nov 20, 2020
deae6d1
Don't delete export automatically on download as there is a deleteexp…
johnthagen Nov 20, 2020
f17f2c9
Remove vestigial code
imnasnainaec Nov 20, 2020
1f61c06
merge
imnasnainaec Nov 20, 2020
95bcee0
Add appropriate text, function for each ExportStatus.
imnasnainaec Nov 20, 2020
4911f55
Require projectId in export and download
imnasnainaec Nov 20, 2020
dac932d
Allow configuration of Cors Origin
johnthagen Nov 20, 2020
c9f76c1
Change handler when projectId changes
imnasnainaec Nov 20, 2020
6adccfc
Merge branch 'master' into signalr
johnthagen Nov 20, 2020
565c865
Simply stop connection after downloadReady notification
imnasnainaec Nov 20, 2020
1c3b536
Merge branch 'signalr' of https://github.com/sillsdev/TheCombine into…
imnasnainaec Nov 20, 2020
81bffa7
Re-enable captcha
imnasnainaec Nov 20, 2020
2ed9c0a
Configure SignalR backend path to use runtime configuration
johnthagen Nov 20, 2020
830e49b
Remove unnecessary file handling on download
imnasnainaec Nov 20, 2020
de7c1f2
Merge branch 'signalr' of https://github.com/sillsdev/TheCombine into…
imnasnainaec Nov 20, 2020
be8bc2d
Don't reuse global var name locally
imnasnainaec Nov 20, 2020
14e318e
Unify the frontend usages of baseURL
johnthagen Nov 21, 2020
25a386e
Support web socket in NGINX configuration
johnthagen Nov 21, 2020
d7ea03c
Comment cleanup
imnasnainaec Nov 21, 2020
203642a
Stop all connections if exportState changes.
imnasnainaec Nov 21, 2020
e3a5e5d
Add ability to cancel in-progress exports.
imnasnainaec Nov 21, 2020
bc84f77
Remove frontend cancel export, lacking backend to match.
imnasnainaec Nov 23, 2020
6f4db7f
Add backend inProgress status to prevent multiple exports from a sing…
imnasnainaec Nov 23, 2020
057674d
Removed unused translation.
imnasnainaec Nov 23, 2020
b3903e5
Fix catch-throw.
imnasnainaec Nov 23, 2020
c69e151
Change readonly to const at top of class.
imnasnainaec Nov 23, 2020
61a6511
Cure colony collapse disorder.
imnasnainaec Nov 23, 2020
4278b21
Merge branch 'master' into signalr
imnasnainaec Nov 24, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions Backend.Tests/Controllers/LiftControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
using System.Text.RegularExpressions;
using Backend.Tests.Mocks;
using BackendFramework.Controllers;
using BackendFramework.Helper;
using static BackendFramework.Helper.FileUtilities;
using BackendFramework.Interfaces;
using BackendFramework.Models;
using BackendFramework.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using NUnit.Framework;

namespace Backend.Tests.Controllers
Expand All @@ -22,6 +24,7 @@ public class LiftControllerTests
private IProjectService _projServ;
private ILiftService _liftService;
private LiftController _liftController;
private IHubContext<CombineHub> _notifyService;
private IPermissionService _permissionService;

[SetUp]
Expand All @@ -31,7 +34,9 @@ public void Setup()
_projServ = new ProjectServiceMock();
_wordrepo = new WordRepositoryMock();
_liftService = new LiftService();
_liftController = new LiftController(_wordrepo, _projServ, _permissionService, _liftService);
_notifyService = new HubContextMock();
_liftController = new LiftController(
_wordrepo, _projServ, _permissionService, _liftService, _notifyService);
_wordService = new WordService(_wordrepo);
}

Expand Down Expand Up @@ -214,10 +219,6 @@ public void TestExportDeleted()
var result = _liftController.DownloadLiftFile(proj.Id, userId).Result as FileContentResult;
Assert.NotNull(result);

// Ensure that downloading a Lift file deletes the temporary in-memory copy.
var notFoundResult = _liftController.DownloadLiftFile(proj.Id, userId).Result as NotFoundObjectResult;
Assert.NotNull(notFoundResult);

// Write LiftFile contents to a temporary directory.
var extractedExportDir = ExtractZipFileContents(result.FileContents);
var exportPath = Path.Combine(extractedExportDir,
Expand All @@ -228,6 +229,11 @@ public void TestExportDeleted()
Assert.That(Regex.Matches(text, "<entry").Count, Is.EqualTo(3));
// There is only one deleted word
Assert.That(text.IndexOf("dateDeleted"), Is.EqualTo(text.LastIndexOf("dateDeleted")));

// Delete the export
_liftController.DeleteLiftFile(userId);
var notFoundResult = _liftController.DownloadLiftFile(proj.Id, userId).Result as NotFoundObjectResult;
Assert.NotNull(notFoundResult);
}

[Test]
Expand Down
74 changes: 74 additions & 0 deletions Backend.Tests/Mocks/HubContextMock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BackendFramework.Helper;
using Microsoft.AspNetCore.SignalR;

namespace Backend.Tests.Mocks
{
/// <summary>
/// A *very* sparse, mostly unimplemented Mock of SignalR HubContext.
/// </summary>
public class HubContextMock : IHubContext<CombineHub>
{
public IHubClients Clients => new HubClientsMock();

public IGroupManager Groups => throw new System.NotImplementedException();
}

public class HubClientsMock : IHubClients
{
public IClientProxy AllExcept(IReadOnlyList<string> excludedConnectionIds)
{
throw new System.NotImplementedException();
}

public IClientProxy Client(string connectionId)
{
throw new System.NotImplementedException();
}

public IClientProxy Clients(IReadOnlyList<string> connectionIds)
{
throw new System.NotImplementedException();
}

public IClientProxy Group(string groupName)
{
throw new System.NotImplementedException();
}

public IClientProxy GroupExcept(string groupName, IReadOnlyList<string> excludedConnectionIds)
{
throw new System.NotImplementedException();
}

public IClientProxy Groups(IReadOnlyList<string> groupNames)
{
throw new System.NotImplementedException();
}

public IClientProxy User(string userId)
{
throw new System.NotImplementedException();
}

public IClientProxy Users(IReadOnlyList<string> userIds)
{
throw new System.NotImplementedException();
}

public IClientProxy All => new ClientProxyMock();
}

public class ClientProxyMock : IClientProxy
{
// Disable this warning as this mock simply needs to return an empty Task, not await anything.
#pragma warning disable 1998
public async Task SendCoreAsync(
#pragma warning restore 1998
string method, object[] args, CancellationToken cancellationToken = new CancellationToken())
{
}
}
}
14 changes: 7 additions & 7 deletions Backend/BackendFramework.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="Data\sdList.txt"/>
<None Remove="Data\sdList.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Data\sdList.txt"/>
<EmbeddedResource Include="Data\sdList.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.5" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.5" />
<PackageReference Include="icu.net" Version="2.6.0" />
<PackageReference Include="Icu4c.Win.Full.Lib" Version="62.1.4-beta"/>
<PackageReference Include="Icu4c.Win.Full.Lib" Version="62.1.4-beta" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="6.7.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.7.1" />
<PackageReference Include="MongoDB.Driver" Version="2.10.4" />
<PackageReference Include="SIL.Core" Version="7.0.0-alpha0822"/>
<PackageReference Include="SIL.Core" Version="7.0.0-alpha0822" />
<PackageReference Include="SIL.Core.Desktop" Version="7.0.0-alpha0822">
<NoWarn>NU1701</NoWarn>
</PackageReference>
Expand All @@ -29,8 +29,8 @@
<PackageReference Include="SIL.Lift" Version="7.0.0-alpha0822">
<NoWarn>NU1701</NoWarn>
</PackageReference>
<PackageReference Include="SIL.WritingSystems" Version="7.0.0-alpha0822"/>
<PackageReference Include="MailKit" Version="2.7.0"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.WebApiCompatShim" Version="2.2.0"/>
<PackageReference Include="SIL.WritingSystems" Version="7.0.0-alpha0822" />
<PackageReference Include="MailKit" Version="2.7.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.WebApiCompatShim" Version="2.2.0" />
</ItemGroup>
</Project>
79 changes: 62 additions & 17 deletions Backend/Controllers/LiftController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using BackendFramework.Helper;
using BackendFramework.Interfaces;
using BackendFramework.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using SIL.Lift.Parsing;
using static BackendFramework.Helper.FileUtilities;

Expand All @@ -24,14 +26,16 @@ public class LiftController : Controller
private readonly ILiftService _liftService;
private readonly IProjectService _projectService;
private readonly IPermissionService _permissionService;
private readonly IHubContext<CombineHub> _notifyService;

public LiftController(IWordRepository repo, IProjectService projServ, IPermissionService permissionService,
ILiftService liftService)
ILiftService liftService, IHubContext<CombineHub> notifyService)
{
_wordRepo = repo;
_projectService = projServ;
_liftService = liftService;
_permissionService = permissionService;
_notifyService = notifyService;
}

/// <summary> Adds data from a zipped directory containing a lift file </summary>
Expand Down Expand Up @@ -186,7 +190,8 @@ public async Task<IActionResult> ExportLiftFile(string projectId)
return await ExportLiftFile(projectId, userId);
}

public async Task<IActionResult> ExportLiftFile(string projectId, string userId)
// These internal methods are extracted for unit testing
internal async Task<IActionResult> ExportLiftFile(string projectId, string userId)
{
if (!_permissionService.HasProjectPermission(HttpContext, Permission.ImportExport))
{
Expand All @@ -199,24 +204,51 @@ public async Task<IActionResult> ExportLiftFile(string projectId, string userId)
return new UnsupportedMediaTypeResult();
}

// Ensure project exists and has words
// Ensure project exists
var proj = _projectService.GetProject(projectId);
if (proj is null)
{
return new NotFoundObjectResult(projectId);
}
var words = await _wordRepo.GetAllWords(projectId);
if (words.Count == 0)

// Check if another export started
if (_liftService.IsExportInProgress(userId))
{
return new BadRequestResult();
return new ConflictResult();
}

// Export the data to a zip, read into memory, and delete zip
var exportedFilepath = CreateLiftExport(projectId);
// Store in-progress status for the export
_liftService.SetExportInProgress(userId, true);

// Store the temporary path to the exported file for user to download later.
_liftService.StoreExport(userId, exportedFilepath);
return new OkObjectResult(projectId);
try
{
// Ensure project has words
var words = await _wordRepo.GetAllWords(projectId);
if (words.Count == 0)
{
_liftService.SetExportInProgress(userId, false);
return new BadRequestResult();
}

// Export the data to a zip, read into memory, and delete zip
var exportedFilepath = CreateLiftExport(projectId);

// Store the temporary path to the exported file for user to download later.
_liftService.StoreExport(userId, exportedFilepath);
await _notifyService.Clients.All.SendAsync("DownloadReady", userId);
return new OkObjectResult(projectId);
}
catch
{
_liftService.SetExportInProgress(userId, false);
throw;
}
}

internal string CreateLiftExport(string projectId)
{
var exportedFilepath = _liftService.LiftExport(projectId, _wordRepo, _projectService);
return exportedFilepath;
}

/// <summary> Downloads project data in zip file </summary>
Expand All @@ -229,7 +261,7 @@ public async Task<IActionResult> DownloadLiftFile(string projectId)
return await DownloadLiftFile(projectId, userId);
}

public async Task<IActionResult> DownloadLiftFile(string projectId, string userId)
internal async Task<IActionResult> DownloadLiftFile(string projectId, string userId)
{
if (!_permissionService.HasProjectPermission(HttpContext, Permission.ImportExport))
{
Expand All @@ -244,18 +276,31 @@ public async Task<IActionResult> DownloadLiftFile(string projectId, string userI
}

var file = await System.IO.File.ReadAllBytesAsync(filePath);
_liftService.DeleteExport(userId);
return File(
file,
"application/zip",
$"LiftExport-{projectId}-{DateTime.Now:yyyy-MM-dd_hh-mm-ss-fff}.zip");
}

// This method is extracted so that it can be unit tested
internal string CreateLiftExport(string projectId)
/// <summary> Delete prepared export </summary>
/// <remarks> GET: v1/projects/{projectId}/words/deleteexport </remarks>
/// <returns> UserId, if successful </returns>
[HttpGet("deleteexport")]
public IActionResult DeleteLiftFile()
{
var exportedFilepath = _liftService.LiftExport(projectId, _wordRepo, _projectService);
return exportedFilepath;
var userId = _permissionService.GetUserId(HttpContext);
return DeleteLiftFile(userId);
}

internal IActionResult DeleteLiftFile(string userId)
{
if (!_permissionService.HasProjectPermission(HttpContext, Permission.ImportExport))
{
return new ForbidResult();
}

_liftService.DeleteExport(userId);
return new OkObjectResult(userId);
}
}

Expand Down
8 changes: 8 additions & 0 deletions Backend/Helper/CombineHub.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.SignalR;

namespace BackendFramework.Helper
{
public class CombineHub : Hub
{
}
}
2 changes: 2 additions & 0 deletions Backend/Interfaces/ILiftService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ ILexiconMerger<LiftObject, LiftEntry, LiftSense, LiftExample> GetLiftImporterExp
void StoreExport(string key, string filePath);
string? RetrieveExport(string key);
bool DeleteExport(string key);
void SetExportInProgress(string key, bool isInProgress);
bool IsExportInProgress(string key);
}
}
Loading