Skip to content

Commit

Permalink
Merge branch 'master' into fix-lift-import-after-audio
Browse files Browse the repository at this point in the history
  • Loading branch information
johnthagen authored Nov 13, 2020
2 parents 0e25e48 + 04279da commit 5dd0cc2
Show file tree
Hide file tree
Showing 57 changed files with 502 additions and 324 deletions.
108 changes: 65 additions & 43 deletions Backend.Tests/Controllers/LiftControllerTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
Expand Down Expand Up @@ -162,6 +163,27 @@ private static FileUpload InitFile(Stream fstream, string filename)
return fileUpload;
}

/// <summary> Extract the binary contents of a zip file to a temporary directory. </summary>
private static string ExtractZipFileContents(byte[] fileContents)
{
var zipFile = Path.GetTempFileName();
File.WriteAllBytes(zipFile, fileContents);
var extractionPath = ExtractZipFile(zipFile, true);
return extractionPath;
}

private static string ExtractZipFile(string zipFilePath, bool deleteZipFile = false)
{
var extractionPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(extractionPath);
ZipFile.ExtractToDirectory(zipFilePath, extractionPath);
if (deleteZipFile)
{
File.Delete(zipFilePath);
}
return extractionPath;
}

private class RoundTripObj
{
public string Language { get; set; }
Expand All @@ -170,7 +192,8 @@ private class RoundTripObj
public string EntryGuid { get; set; }
public string SenseGuid { get; set; }

public RoundTripObj(string lang, List<string> audio, int words, string entryGuid = "", string senseGuid = "")
public RoundTripObj(
string lang, List<string> audio, int words, string entryGuid = "", string senseGuid = "")
{
Language = lang;
AudioFiles = audio;
Expand Down Expand Up @@ -200,10 +223,12 @@ public void TestExportDeleted()
_wordService.Update(proj.Id, wordToUpdate.Id, word);
_wordService.DeleteFrontierWord(proj.Id, wordToDelete.Id);

var result = _liftController.ExportLiftFile(proj.Id).Result;
var result = _liftController.ExportLiftFile(proj.Id).Result as OkObjectResult;
var fileContents = Convert.FromBase64String(result.Value as string);

var combinePath = FileUtilities.GenerateFilePath(FileUtilities.FileType.Dir, true, "", "");
var exportPath = Path.Combine(combinePath, proj.Id, "Export", "LiftExport",
// Write LiftFile contents to a temporary directory.
var extractedExportDir = ExtractZipFileContents(fileContents);
var exportPath = Path.Combine(extractedExportDir,
Path.Combine("Lift", "NewLiftFile.lift"));
var text = File.ReadAllText(exportPath, Encoding.UTF8);
//TODO: Add SIL or other XML assertion library and verify with xpath that the correct entries are kept vs deleted
Expand Down Expand Up @@ -248,10 +273,9 @@ public void TestRoundtrip()
"e44420dd-a867-4d71-a43f-e472fd3a8f82" /*id of its first sense*/);
fileMapping.Add("SingleEntryLiftWithTwoSound.zip", singleEntryLiftWithTwoSound);

foreach (var dataSet in fileMapping)
foreach (var (filename, roundTripContents) in fileMapping)
{
var actualFilename = dataSet.Key;
var pathToStartZip = Path.Combine(pathToStartZips, actualFilename);
var pathToStartZip = Path.Combine(pathToStartZips, filename);

// Upload the zip file

Expand All @@ -263,50 +287,47 @@ public void TestRoundtrip()
if (File.Exists(pathToStartZip))
{
var fstream = File.OpenRead(pathToStartZip);
var fileUpload = InitFile(fstream, actualFilename);
var fileUpload = InitFile(fstream, filename);

// Make api call
var result = _liftController.UploadLiftFile(proj.Id, fileUpload).Result;
Assert.That(!(result is BadRequestObjectResult));

proj = _projServ.GetProject(proj.Id).Result;
Assert.AreEqual(proj.VernacularWritingSystem.Bcp47, dataSet.Value.Language);
Assert.AreEqual(proj.VernacularWritingSystem.Bcp47, roundTripContents.Language);

fstream.Close();

var allWords = _wordrepo.GetAllWords(proj.Id).Result;
Assert.AreEqual(allWords.Count, dataSet.Value.NumOfWords);
Assert.AreEqual(allWords.Count, roundTripContents.NumOfWords);
// We are currently only testing guids on the single-entry data sets
if (dataSet.Value.EntryGuid != "" && allWords.Count == 1)
if (roundTripContents.EntryGuid != "" && allWords.Count == 1)
{
Assert.AreEqual(allWords[0].Guid.ToString(), dataSet.Value.EntryGuid);
if (dataSet.Value.SenseGuid != "")
Assert.AreEqual(allWords[0].Guid.ToString(), roundTripContents.EntryGuid);
if (roundTripContents.SenseGuid != "")
{
Assert.AreEqual(allWords[0].Senses[0].Guid.ToString(), dataSet.Value.SenseGuid);
Assert.AreEqual(allWords[0].Senses[0].Guid.ToString(), roundTripContents.SenseGuid);
}
}

// Export
var exportedFilePath = _liftController.CreateLiftExport(proj.Id);
var exportedDirectory = Path.GetDirectoryName(exportedFilePath);
var exportedDirectory = ExtractZipFile(exportedFilePath, false);

// Assert the file was created with desired heirarchy
Assert.That(Directory.Exists(exportedDirectory));
Assert.That(Directory.Exists(Path.Combine(exportedDirectory, "LiftExport", "Lift", "audio")));
foreach (var audioFile in dataSet.Value.AudioFiles)
Assert.That(Directory.Exists(Path.Combine(exportedDirectory, "Lift", "audio")));
foreach (var audioFile in roundTripContents.AudioFiles)
{
Assert.That(File.Exists(Path.Combine(
exportedDirectory, "LiftExport", "Lift", "audio", audioFile)));
exportedDirectory, "Lift", "audio", audioFile)));
}
Assert.That(Directory.Exists(Path.Combine(exportedDirectory, "LiftExport", "Lift", "WritingSystems")));
Assert.That(Directory.Exists(Path.Combine(exportedDirectory, "Lift", "WritingSystems")));
Assert.That(File.Exists(Path.Combine(
exportedDirectory,
"LiftExport", "Lift", "WritingSystems", dataSet.Value.Language + ".ldml")));
Assert.That(File.Exists(Path.Combine(exportedDirectory, "LiftExport", "Lift", "NewLiftFile.lift")));
var dirList = new List<string>(
Directory.GetDirectories(Path.GetDirectoryName(exportedDirectory)));
dirList.Remove(exportedDirectory);
Assert.That(Directory.Exists(Path.Combine(Path.GetDirectoryName(exportedDirectory), dirList.Single())));
"Lift", "WritingSystems", roundTripContents.Language + ".ldml")));
Assert.That(File.Exists(Path.Combine(exportedDirectory, "Lift", "NewLiftFile.lift")));
Directory.Delete(exportedDirectory, true);

_wordrepo.DeleteAllWords(proj.Id);

Expand All @@ -319,50 +340,51 @@ public void TestRoundtrip()

// Generate api parameter with filestream
fstream = File.OpenRead(exportedFilePath);
fileUpload = InitFile(fstream, actualFilename);
fileUpload = InitFile(fstream, filename);

// Make api call
var result2 = _liftController.UploadLiftFile(proj2.Id, fileUpload).Result;
Assert.That(!(result is BadRequestObjectResult));

proj2 = _projServ.GetProject(proj2.Id).Result;
Assert.AreEqual(proj2.VernacularWritingSystem.Bcp47, dataSet.Value.Language);
Assert.AreEqual(proj2.VernacularWritingSystem.Bcp47, roundTripContents.Language);

fstream.Close();

// Clean up zip file.
File.Delete(exportedFilePath);

allWords = _wordrepo.GetAllWords(proj2.Id).Result;
Assert.AreEqual(allWords.Count, dataSet.Value.NumOfWords);
Assert.AreEqual(allWords.Count, roundTripContents.NumOfWords);
// We are currently only testing guids on the single-entry data sets
if (dataSet.Value.EntryGuid != "" && allWords.Count == 1)
if (roundTripContents.EntryGuid != "" && allWords.Count == 1)
{
Assert.AreEqual(allWords[0].Guid.ToString(), dataSet.Value.EntryGuid);
if (dataSet.Value.SenseGuid != "")
Assert.AreEqual(allWords[0].Guid.ToString(), roundTripContents.EntryGuid);
if (roundTripContents.SenseGuid != "")
{
Assert.AreEqual(allWords[0].Senses[0].Guid.ToString(), dataSet.Value.SenseGuid);
Assert.AreEqual(allWords[0].Senses[0].Guid.ToString(), roundTripContents.SenseGuid);
}
}

// Export
exportedFilePath = _liftController.CreateLiftExport(proj2.Id);
exportedDirectory = Path.GetDirectoryName(exportedFilePath);
exportedDirectory = ExtractZipFile(exportedFilePath);

// Assert the file was created with desired hierarchy
Assert.That(Directory.Exists(exportedDirectory));
Assert.That(Directory.Exists(Path.Combine(exportedDirectory, "LiftExport", "Lift", "audio")));
foreach (var audioFile in dataSet.Value.AudioFiles)
Assert.That(Directory.Exists(Path.Combine(exportedDirectory, "Lift", "audio")));
foreach (var audioFile in roundTripContents.AudioFiles)
{
var path = Path.Combine(exportedDirectory, "LiftExport", "Lift", "audio", audioFile);
var path = Path.Combine(exportedDirectory, "Lift", "audio", audioFile);
Assert.That(File.Exists(path),
"The file " + audioFile + " can not be found at this path: " + path);
$"The file {audioFile} can not be found at this path: {path}");
}
Assert.That(Directory.Exists(Path.Combine(exportedDirectory, "LiftExport", "Lift", "WritingSystems")));
Assert.That(Directory.Exists(Path.Combine(exportedDirectory, "Lift", "WritingSystems")));
Assert.That(File.Exists(Path.Combine(
exportedDirectory,
"LiftExport", "Lift", "WritingSystems", dataSet.Value.Language + ".ldml")));
Assert.That(File.Exists(Path.Combine(exportedDirectory, "LiftExport", "Lift", "NewLiftFile.lift")));
dirList = new List<string>(Directory.GetDirectories(Path.GetDirectoryName(exportedDirectory)));
dirList.Remove(exportedDirectory);
Assert.That(Directory.Exists(Path.Combine(Path.GetDirectoryName(exportedDirectory), dirList.Single())));
"Lift", "WritingSystems", roundTripContents.Language + ".ldml")));
Assert.That(File.Exists(Path.Combine(exportedDirectory, "Lift", "NewLiftFile.lift")));
Directory.Delete(exportedDirectory, true);

_wordrepo.DeleteAllWords(proj.Id);
}
Expand Down
10 changes: 9 additions & 1 deletion Backend/Controllers/LiftController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ public async Task<IActionResult> UploadLiftFile(string projectId, [FromForm] Fil
Directory.CreateDirectory(extractDir);
ZipFile.ExtractToDirectory(fileUpload.FilePath, extractDir);

// Clean up the temporary zip file
System.IO.File.Delete(fileUpload.FilePath);

// Check number of directories extracted
var directoriesExtracted = Directory.GetDirectories(extractDir).Except(existingDirectories).ToArray();
var extractedDirPath = "";
Expand Down Expand Up @@ -227,9 +230,14 @@ public async Task<IActionResult> ExportLiftFile(string projectId)
}
// Export the data to a zip directory
var exportedFilepath = CreateLiftExport(projectId);

var file = await System.IO.File.ReadAllBytesAsync(exportedFilepath);

// Clean up temporary file after reading it.
System.IO.File.Delete(exportedFilepath);
GC.Collect();

var encodedFile = Convert.ToBase64String(file);

return new OkObjectResult(encodedFile);
}

Expand Down
4 changes: 4 additions & 0 deletions Backend/Services/LiftApiServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ private static string GetProjectDir(string projectId)
}

/// <summary> Exports information from a project to a lift package zip </summary>
/// <returns> Path to compressed zip file containing export. </returns>
public string LiftExport(string projectId, IWordRepository wordRepo, IProjectService projService)
{
// Generate the zip dir.
Expand Down Expand Up @@ -275,6 +276,9 @@ public string LiftExport(string projectId, IWordRepository wordRepo, IProjectSer
Path.Combine($"LiftExportCompressed-{proj.Id}_{DateTime.Now:yyyy-MM-dd_hh-mm-ss}.zip"));
ZipFile.CreateFromDirectory(Path.GetDirectoryName(zipDir), destinationFileName);

// Clean up the temporary folder structure that was compressed.
Directory.Delete(Path.Combine(exportDir, "LiftExport"), true);

return destinationFileName;
}

Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "thecombine",
"version": "0.4.1-alpha.0",
"version": "0.5.1-alpha.0",
"license": "MIT",
"private": true,
"scripts": {
Expand Down
8 changes: 6 additions & 2 deletions src/backend/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import axios from "axios";

import authHeader from "../components/Login/AuthHeaders";
import history from "../history";
import history, { path } from "../history";
import { Goal, GoalType } from "../types/goals";
import { Project } from "../types/project";
import { RuntimeConfig } from "../types/runtimeConfig";
Expand All @@ -28,7 +28,7 @@ backendServer.interceptors.response.use(
},
(err) => {
if (err.response && err.response.status === 401) {
history.push("/login");
history.push(path.login);
}
return Promise.reject(err);
}
Expand Down Expand Up @@ -268,6 +268,10 @@ export async function getProject(id?: string): Promise<Project> {
return resp.data;
}

export async function getProjectName(id?: string): Promise<string> {
return (await getProject(id)).name;
}

export async function updateProject(project: Project) {
let resp = await backendServer.put(`projects/${project.id}`, project, {
headers: authHeader(),
Expand Down
48 changes: 32 additions & 16 deletions src/components/App/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@ import React from "react";
import { Route, Switch } from "react-router-dom";

//TC modules
import { path } from "../../history";
import DataEntry from "../DataEntry";
import { GoalRoute } from "../GoalRoute/component";
import GoalRoute from "../GoalRoute/component";
import Login from "../Login/LoginPage";
import Register from "../Login/RegisterPage";
import PageNotFound from "../PageNotFound/component";
import PasswordReset from "../PasswordReset/ResetPage";
import ResetRequest from "../PasswordReset/RequestPage";
import PageNotFound from "../PageNotFound/component";
import { PrivateRoute } from "../PrivateRoute";
import ProjectSettings from "../ProjectSettings";
import PrivateRoute from "../PrivateRoute";
import ProjectInvite from "../ProjectInvite";
import ProjectScreen from "../ProjectScreen/ProjectScreenComponent";
import ProjectSettings from "../ProjectSettings";
import SiteSettings from "../SiteSettings/SiteSettingsComponent";
import UserSettings from "../UserSettings/UserSettings";
import ProjectInvite from "../ProjectInvite";

/**
* The top-level component
Expand All @@ -25,21 +26,36 @@ export default class App extends React.Component {
return (
<div className="App">
<Switch>
<PrivateRoute exact path="/" component={ProjectScreen} />
<PrivateRoute exact path="/data-entry" component={DataEntry} />
<PrivateRoute
exact
path="/project-settings"
path={path.projScreen}
component={ProjectScreen}
/>
<PrivateRoute exact path={path.dataEntry} component={DataEntry} />
<PrivateRoute
exact
path={path.projSettings}
component={ProjectSettings}
/>
<PrivateRoute exact path="/site-settings" component={SiteSettings} />
<PrivateRoute exact path="/user-settings" component={UserSettings} />
<PrivateRoute path="/goals" component={GoalRoute} />
<Route path="/login" component={Login} />
<Route path="/register" component={Register} />
<Route path="/forgot/reset/:token" component={PasswordReset} />
<Route path="/forgot/request" component={ResetRequest} />
<Route path="/invite/:project/:token" component={ProjectInvite} />
<PrivateRoute
exact
path={path.siteSettings}
component={SiteSettings}
/>
<PrivateRoute
exact
path={path.userSettings}
component={UserSettings}
/>
<PrivateRoute path={path.goals} component={GoalRoute} />
<Route path={path.login} component={Login} />
<Route path={path.register} component={Register} />
<Route path={`${path.pwReset}/:token`} component={PasswordReset} />
<Route path={path.pwRequest} component={ResetRequest} />
<Route
path={`${path.projInvite}/:project/:token`}
component={ProjectInvite}
/>
<Route component={PageNotFound} />
</Switch>
</div>
Expand Down
7 changes: 4 additions & 3 deletions src/components/AppBar/Logo.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import React from "react";
import { Button } from "@material-ui/core";
import React from "react";

import history, { path } from "../../history";
import logo from "../../resources/CombineLogoV1.png";
import smallLogo from "../../resources/CombineSmallLogoV1.png";
import history from "../../history";

/** A button that redirects to the home page */
export default function Logo() {
return (
<Button
onClick={() => {
history.push("/");
history.push(path.projScreen);
}}
>
<img
Expand Down
Loading

0 comments on commit 5dd0cc2

Please sign in to comment.