Fixes asset caching. Restores the cached asset urls if sending to the quiz...

Fixes asset caching. Restores the cached asset urls if sending to the quiz owner and in the export file
parent 00cc97e1
......@@ -6,15 +6,6 @@ import { AssetModel, AssetModelItem } from '../models/AssetModel';
import { AbstractDAO } from './AbstractDAO';
class AssetDAO extends AbstractDAO {
constructor() {
super();
AssetModel.find().exec().then(assets => {
assets.forEach(asset => this.addAsset(asset));
});
}
public static getInstance(): AssetDAO {
if (!this.instance) {
this.instance = new AssetDAO();
......@@ -47,6 +38,10 @@ class AssetDAO extends AbstractDAO {
return AssetModel.findOne({ url }).exec();
}
public async getAssetByDigestAsLean(digest: string): Promise<IAssetSerialized> {
return AssetModel.findOne({ digest }).lean().exec();
}
private getAssetById(id: ObjectID): Promise<AssetModelItem> {
return AssetModel.findById({ id }).exec();
}
......
......@@ -3,7 +3,7 @@ import * as http from 'http';
import * as MessageFormat from 'messageformat';
import { QuestionType } from '../enums/QuestionType';
import { IExcelWorkbook, IExcelWorksheet } from '../interfaces/iExcel';
import { IQuizBase } from '../interfaces/quizzes/IQuizEntity';
import { IQuiz, IQuizBase } from '../interfaces/quizzes/IQuizEntity';
import { FreeTextExcelWorksheet } from './FreeTextExcelWorksheet';
import { ExcelTheme } from './lib/excel_default_styles';
......@@ -25,7 +25,7 @@ export class ExcelWorkbook implements IExcelWorkbook {
private readonly _mf: MessageFormat;
private readonly _quiz: IQuizBase;
constructor({ themeName, quiz, translation, mf }: { themeName: string, quiz: IQuizBase, translation: string, mf: MessageFormat }) {
constructor({ themeName, quiz, translation, mf }: { themeName: string, quiz: IQuiz, translation: string, mf: MessageFormat }) {
this._wb = new xlsx.Workbook({
jszip: {
compression: 'DEFLATE',
......
import * as Hex from 'crypto-js/enc-hex';
import { Document } from 'mongoose';
import * as requestPromise from 'request-promise-native';
import AssetDAO from '../../db/AssetDAO';
import { IAnswer } from '../../interfaces/answeroptions/IAnswerEntity';
import { IQuestion } from '../../interfaces/questions/IQuestion';
import { IQuiz } from '../../interfaces/quizzes/IQuizEntity';
import { AssetModel } from '../../models/AssetModel';
import { AssetModel, AssetModelItem } from '../../models/AssetModel';
import LoggerService from '../../services/LoggerService';
import { staticStatistics } from '../../statistics';
import { asyncForEach } from '../async-for-each';
......@@ -12,6 +12,49 @@ import { asyncForEach } from '../async-for-each';
const sha256 = require('crypto-js/sha256');
export const assetsUrlRegex = /[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/gi;
const assetsPathUrlRegex = '(' + staticStatistics.rewriteAssetCacheUrl + '[\/a-z]*([0-9a-z]*))';
export function GetAssetUrlByDigest(digest: string): Promise<Document & AssetModelItem> {
return AssetModel.findOne({ digest }, {
_id: 0,
url: 1,
}).exec();
}
export async function MatchAssetCachedQuiz(quiz: IQuiz): Promise<IQuiz> {
quiz.questionList = await Promise.all(quiz.questionList.map(async question => {
question.answerOptionList = await Promise.all(question.answerOptionList.map(async answer => {
const answerMatched = answer.answerText.matchAll(new RegExp(assetsPathUrlRegex, 'gi'));
let answerTextMatcher = answerMatched.next();
while (!answerTextMatcher.done) {
const answerTextDbResult = (await GetAssetUrlByDigest(answerTextMatcher.value[2]));
if (answerTextDbResult) {
const url = answerTextDbResult.url;
answer.answerText = answer.answerText.replace(answerTextMatcher.value[0], url);
}
answerTextMatcher = answerMatched.next();
}
return answer;
}));
const questionTextMatched = question.questionText.matchAll(new RegExp(assetsPathUrlRegex, 'gi'));
let questionTextMatcher = questionTextMatched.next();
while (!questionTextMatcher.done) {
const quesitonTextDbResult = (await GetAssetUrlByDigest(questionTextMatcher.value[2]));
if (quesitonTextDbResult) {
const url = quesitonTextDbResult.url;
question.questionText = question.questionText.replace(questionTextMatcher.value[0], url);
}
questionTextMatcher = questionTextMatched.next();
}
return question;
}));
return quiz;
}
export function MatchTextToAssetsDb(value: string): Promise<string> {
const acceptedFileTypes = [/image\/*/];
......@@ -21,12 +64,13 @@ export function MatchTextToAssetsDb(value: string): Promise<string> {
return new Promise<string>(resolve => {
if (!foundUrls) {
resolve(value);
return;
}
foundUrls.forEach((foundUrl: string) => {
return asyncForEach(foundUrls, async (foundUrl: string) => {
const digest = Hex.stringify(sha256(foundUrl));
const parsedResult = value.replace(foundUrl, `${assetsBasePath}/${digest}`);
const exists = AssetDAO.getAssetByDigest(digest);
const exists = await AssetDAO.getAssetByDigest(digest);
if (exists) {
resolve(parsedResult);
return;
......@@ -50,11 +94,11 @@ export function MatchTextToAssetsDb(value: string): Promise<string> {
const buffer = Buffer.from(response.body, 'utf8');
return AssetModel.create({
return AssetDAO.addAsset({
url: foundUrl,
digest,
mimeType: contentType,
data: Buffer.from(buffer),
data: buffer,
});
}).then(() => resolve(parsedResult)).catch((err) => {
......@@ -64,34 +108,3 @@ export function MatchTextToAssetsDb(value: string): Promise<string> {
});
});
}
export async function parseCachedAssetQuiz(cacheAwareQuestions: Array<IQuestion>): Promise<void> {
const assetsBasePath = `${staticStatistics.rewriteAssetCacheUrl}/lib/cache/quiz/assets`;
await asyncForEach(cacheAwareQuestions, async (question: IQuestion) => {
const matchedQuestionText = question.questionText.match(assetsUrlRegex);
if (matchedQuestionText) {
await asyncForEach(matchedQuestionText, async (matchedValueElement: string) => {
const existing = await AssetDAO.getAssetByUrl(matchedValueElement);
if (!existing) {
return;
}
const cachedUrl = `${assetsBasePath}/${existing.digest}`;
question.questionText = question.questionText.replace(matchedValueElement, cachedUrl);
});
}
await asyncForEach(question.answerOptionList, (async (answerOption: IAnswer) => {
const matchedAnswerText = answerOption.answerText.match(assetsUrlRegex);
if (matchedAnswerText) {
await asyncForEach(matchedAnswerText, (async (matchedValueElement: string) => {
const existing = await AssetDAO.getAssetByUrl(matchedValueElement);
if (!existing) {
return;
}
const cachedUrl = `${assetsBasePath}/${existing.digest}`;
answerOption.answerText = answerOption.answerText.replace(matchedValueElement, cachedUrl);
}));
}
}));
});
}
......@@ -6,7 +6,10 @@ import LoggerService from '../services/LoggerService';
@index({ digest: 1 }, { unique: true })
export class AssetModelItem implements IAssetSerialized {
@prop({ required: true }) public data: Uint8Array;
@prop({
required: true,
_id: false,
}) public data: Buffer;
@prop() public digest: string;
@prop() public url: string;
@prop() public mimeType: string;
......
......@@ -17,7 +17,7 @@ import { MessageProtocol, StatusProtocol } from '../../enums/Message';
import { IMessage } from '../../interfaces/communication/IMessage';
import { IQuiz } from '../../interfaces/quizzes/IQuizEntity';
import { ICasData } from '../../interfaces/users/ICasData';
import { MatchTextToAssetsDb } from '../../lib/cache/assets';
import { MatchAssetCachedQuiz, MatchTextToAssetsDb } from '../../lib/cache/assets';
import { UserModelItem } from '../../models/UserModelItem/UserModel';
import { AuthService } from '../../services/AuthService';
import LoggerService from '../../services/LoggerService';
......@@ -259,7 +259,7 @@ export class LibRouter extends AbstractRouter {
@Get('/cache/quiz/assets/:digest')
public async getCache(@Param('digest') digest: string, @Res() response: Response): Promise<ArrayBufferLike> {
const doc = await AssetDAO.getAssetByDigest(digest);
const doc = await AssetDAO.getAssetByDigestAsLean(digest);
if (!doc || !doc.data) {
throw new NotFoundError(`Malformed request received -> ${digest}`);
}
......@@ -379,7 +379,9 @@ export class LibRouter extends AbstractRouter {
if (!token || typeof token !== 'string' || token.length === 0) {
token = await AuthService.generateToken(user);
await UserDAO.updateUser(user.id, { token });
const quizzes = (await QuizDAO.getQuizzesByPrivateKey(user.privateKey)).map(quiz => quiz.toJSON());
const quizzes: Array<IQuiz> = await Promise.all((await QuizDAO.getQuizzesByPrivateKey(user.privateKey)).map(quiz => quiz.toJSON()).map(quiz => {
return MatchAssetCachedQuiz(quiz);
}));
return {
status: StatusProtocol.Success,
......
......@@ -32,7 +32,7 @@ import { IMessage } from '../../interfaces/communication/IMessage';
import { IQuizStatusPayload } from '../../interfaces/IQuizStatusPayload';
import { IQuiz } from '../../interfaces/quizzes/IQuizEntity';
import { asyncForEach } from '../../lib/async-for-each';
import { MatchTextToAssetsDb } from '../../lib/cache/assets';
import { MatchAssetCachedQuiz, MatchTextToAssetsDb } from '../../lib/cache/assets';
import { Leaderboard } from '../../lib/leaderboard/leaderboard';
import { QuizModelItem } from '../../models/quiz/QuizModelItem';
import { settings, staticStatistics } from '../../statistics';
......@@ -444,10 +444,17 @@ export class QuizRouter extends AbstractRouter {
if (!quiz) {
throw new BadRequestError(MessageProtocol.InvalidParameters);
}
const existingQuiz = await QuizDAO.getQuizByName(quiz.name);
if (existingQuiz && existingQuiz.privateKey !== privateKey) {
throw new UnauthorizedError(MessageProtocol.InsufficientPermissions);
}
const activeQuizzesAmount = await QuizDAO.getActiveQuizzes();
if (activeQuizzesAmount.length >= settings.public.limitActiveQuizzes) {
throw new BadRequestError(MessageProtocol.TooMuchActiveQuizzes);
}
if (settings.public.createQuizPasswordRequired) {
if (!serverPassword) {
throw new UnauthorizedError(MessageProtocol.ServerPasswordRequired);
......@@ -486,12 +493,7 @@ export class QuizRouter extends AbstractRouter {
},
})));
const existingQuiz = await QuizDAO.getQuizByName(quiz.name);
if (existingQuiz) {
if (existingQuiz.privateKey !== privateKey) {
throw new UnauthorizedError(MessageProtocol.InsufficientPermissions);
}
await QuizDAO.updateQuiz(existingQuiz.id, quiz);
return (await QuizDAO.getQuizByName(quiz.name)).toJSON();
} else {
......@@ -595,12 +597,12 @@ export class QuizRouter extends AbstractRouter {
return;
}
// TODO: The quiz contains the rewritten cached asset urls. Restore them to the original value!
const parsedQuiz = await MatchAssetCachedQuiz(activeQuiz.toJSON());
const wb = new ExcelWorkbook({
themeName,
translation,
quiz: activeQuiz,
quiz: parsedQuiz,
mf: res.__mf,
});
const date: Date = new Date();
......
......@@ -6,6 +6,7 @@
"experimentalDecorators": true,
"lib": [
"esnext",
"es2020",
"dom"
],
"module": "commonjs",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment