2018-12-18 21:18:06 +01:00
|
|
|
const path = require('path');
|
|
|
|
const fs = require('fs');
|
|
|
|
const express = require('express');
|
|
|
|
const http = require('http');
|
2019-01-07 19:22:27 +01:00
|
|
|
const https = require('https');
|
2019-01-07 21:17:37 +01:00
|
|
|
const SocketIoServer = require('socket.io');
|
2018-12-26 21:48:13 +01:00
|
|
|
const filenamify = require('filenamify');
|
|
|
|
const unusedFilename = require('unused-filename');
|
2018-12-27 23:47:06 +01:00
|
|
|
const striptags = require('striptags');
|
2018-12-18 21:18:06 +01:00
|
|
|
|
|
|
|
const settings = require('./settings.json');
|
2019-01-07 19:22:27 +01:00
|
|
|
const privateKey = settings.sslPrivateKey ? fs.readFileSync(settings.sslPrivateKey, 'utf8') : null;
|
|
|
|
const certificate = settings.sslCertificate ? fs.readFileSync(settings.sslCertificate, 'utf8') : null;
|
|
|
|
const ca = settings.sslCertificateAuthority ? fs.readFileSync(settings.sslCertificateAuthority, 'utf8') : null;
|
2018-12-18 21:18:06 +01:00
|
|
|
|
|
|
|
function Server () {
|
|
|
|
this.server = express();
|
|
|
|
this.http = http.Server(this.server);
|
2019-01-07 19:22:27 +01:00
|
|
|
this.https = privateKey && certificate ? https.createServer({ key: privateKey, cert: certificate, ca }, this.server) : null;
|
2019-01-07 21:17:37 +01:00
|
|
|
this.io = new SocketIoServer();
|
|
|
|
if (!settings.forceHTTPS) {
|
|
|
|
this.io.attach(this.http);
|
|
|
|
}
|
|
|
|
if (this.https) {
|
|
|
|
this.io.attach(this.https);
|
|
|
|
}
|
2018-12-18 21:18:06 +01:00
|
|
|
|
|
|
|
this.fileLocation = path.resolve(settings.fileLocation);
|
|
|
|
this.historyLocation = path.resolve(settings.historyLocation);
|
|
|
|
|
2018-12-27 07:36:47 +01:00
|
|
|
this.templateCache = {};
|
|
|
|
|
2019-01-04 00:38:22 +01:00
|
|
|
this.connections = 0;
|
2018-12-26 21:48:13 +01:00
|
|
|
this.takenBooks = [];
|
|
|
|
|
2019-01-10 23:20:48 +01:00
|
|
|
require('./routes/middleware')(this);
|
2018-12-26 21:48:13 +01:00
|
|
|
|
2019-01-10 23:20:48 +01:00
|
|
|
require('./routes/get_home')(this);
|
2018-12-26 21:48:13 +01:00
|
|
|
|
2019-01-10 23:20:48 +01:00
|
|
|
require('./routes/get_give')(this);
|
|
|
|
require('./routes/post_give')(this);
|
2018-12-26 21:48:13 +01:00
|
|
|
|
2019-01-10 23:20:48 +01:00
|
|
|
require('./routes/get_history')(this);
|
2018-12-18 21:18:06 +01:00
|
|
|
|
2019-01-10 23:20:48 +01:00
|
|
|
require('./routes/get_about')(this);
|
2018-12-18 21:18:06 +01:00
|
|
|
|
2019-01-10 23:20:48 +01:00
|
|
|
require('./routes/get_tools')(this);
|
|
|
|
require('./routes/post_tools')(this);
|
2019-01-04 23:08:41 +01:00
|
|
|
|
2019-01-10 23:20:48 +01:00
|
|
|
require('./routes/socketio')(this);
|
2018-12-18 21:18:06 +01:00
|
|
|
}
|
|
|
|
|
2018-12-26 23:03:21 +01:00
|
|
|
Server.prototype.fillTemplate = function (file, templateVars = {}) {
|
2018-12-27 07:36:47 +01:00
|
|
|
let data;
|
|
|
|
if (this.templateCache.hasOwnProperty(file)) {
|
|
|
|
data = this.templateCache[file];
|
|
|
|
} else {
|
2019-01-10 23:20:48 +01:00
|
|
|
data = fs.readFileSync(path.resolve(file), 'utf8');
|
2018-12-27 07:36:47 +01:00
|
|
|
}
|
2018-12-26 23:03:21 +01:00
|
|
|
if (data) {
|
2018-12-27 07:36:47 +01:00
|
|
|
if (!this.templateCache.hasOwnProperty(file)) {
|
|
|
|
this.templateCache[file] = data;
|
|
|
|
}
|
|
|
|
|
2018-12-27 18:55:59 +01:00
|
|
|
let filledTemplate = data.replace(/\{\{siteTitle\}\}/g, settings.siteTitle)
|
|
|
|
.replace(/\{\{titleSeparator\}\}/g, settings.titleSeparator)
|
|
|
|
.replace(/\{\{allowedFormats\}\}/g, settings.allowedFormats.join(','))
|
2018-12-26 23:03:21 +01:00
|
|
|
.replace(/\{\{maxFileSize\}\}/g, (settings.maxFileSize > 0 ? settings.maxFileSize + 'MB' : 'no'));
|
|
|
|
|
|
|
|
for (let templateVar in templateVars) {
|
|
|
|
const regExp = new RegExp('\{\{' + templateVar + '\}\}', 'g')
|
2018-12-27 00:36:35 +01:00
|
|
|
filledTemplate = filledTemplate.replace(regExp, templateVars[templateVar]);
|
2018-12-26 23:03:21 +01:00
|
|
|
}
|
|
|
|
|
2018-12-27 00:36:35 +01:00
|
|
|
// If any template variable is not provided, don't even render them.
|
|
|
|
filledTemplate = filledTemplate.replace(/\{\{[a-zA-Z0-9\-_]+\}\}/g, '');
|
|
|
|
|
2018-12-26 23:03:21 +01:00
|
|
|
return filledTemplate;
|
|
|
|
}
|
|
|
|
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2019-01-04 23:08:41 +01:00
|
|
|
Server.prototype.replaceBodyWithTooManyBooksWarning = function (body) {
|
|
|
|
if (settings.maxLibrarySize > 0) {
|
|
|
|
const numberOfBooks = fs.readdirSync(this.fileLocation).filter(fileName => fileName.includes('.json')).length;
|
|
|
|
if (numberOfBooks >= settings.maxLibrarySize) {
|
|
|
|
body = this.fillTemplate('./templates/elements/messageBox.html', {
|
|
|
|
style: 'is-danger',
|
|
|
|
title: 'Library Full',
|
|
|
|
message: 'Sorry, the library has reached its maximum capacity for books! You will need to wait until a book is taken before a new one can be added.',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return body;
|
|
|
|
}
|
|
|
|
|
2018-12-27 00:36:35 +01:00
|
|
|
Server.prototype.addBook = function (uploadData = {}, success = () => {}, error = () => {}) {
|
2018-12-26 21:48:13 +01:00
|
|
|
const { book } = uploadData;
|
2018-12-28 00:13:48 +01:00
|
|
|
|
|
|
|
// If the file is too big, error out.
|
|
|
|
if (book.truncated === true) {
|
|
|
|
delete book;
|
|
|
|
return error('The file provided is too big');
|
|
|
|
}
|
|
|
|
|
2018-12-26 21:48:13 +01:00
|
|
|
const bookId = this.uuid4();
|
|
|
|
const bookPath = path.resolve(this.fileLocation, bookId);
|
|
|
|
|
|
|
|
const bookData = {
|
2018-12-27 23:47:06 +01:00
|
|
|
title: striptags(uploadData.title.trim()),
|
|
|
|
author: striptags(uploadData.author.trim()),
|
|
|
|
summary: striptags(uploadData.summary.trim().replace(/\r\n/g, '\n')),
|
|
|
|
contributor: striptags(uploadData.contributor.trim()),
|
2018-12-27 22:54:11 +01:00
|
|
|
added: Date.now(),
|
2018-12-26 21:48:13 +01:00
|
|
|
fileType: book.name.substr(book.name.lastIndexOf('.')),
|
|
|
|
}
|
|
|
|
|
2018-12-26 23:03:21 +01:00
|
|
|
const bookFilePath = unusedFilename.sync(path.resolve(bookPath + bookData.fileType));
|
|
|
|
return book.mv(bookFilePath, function (err) {
|
|
|
|
if (err) {
|
|
|
|
console.log(err);
|
2018-12-27 00:36:35 +01:00
|
|
|
error(err);
|
2018-12-26 23:03:21 +01:00
|
|
|
} else {
|
|
|
|
const bookDataPath = unusedFilename.sync(path.resolve(bookPath + '.json'));
|
|
|
|
fs.writeFileSync(bookDataPath, JSON.stringify(bookData));
|
2018-12-27 00:36:35 +01:00
|
|
|
success();
|
2018-12-27 20:40:03 +01:00
|
|
|
// console.log('uploaded ' + bookData.title + ' to ' + bookFilePath + ', and saved metadata to ' + bookDataPath);
|
2018-12-26 21:48:13 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
Server.prototype.takeBook = function (bookId, socketId) {
|
|
|
|
return this.checkId(bookId, (bookPath, bookDataPath, bookData) => {
|
|
|
|
const bookName = filenamify(bookData.title);
|
2018-12-26 23:03:21 +01:00
|
|
|
const newFileName = unusedFilename.sync(path.resolve(this.fileLocation, bookName + bookData.fileType));
|
2018-12-26 21:48:13 +01:00
|
|
|
bookData.fileName = newFileName;
|
|
|
|
fs.renameSync(bookPath, newFileName);
|
|
|
|
fs.writeFileSync(bookDataPath, JSON.stringify(bookData));
|
2018-12-27 20:40:03 +01:00
|
|
|
this.takenBooks.push({ socketId, bookId });
|
|
|
|
return newFileName.replace(/\\/g, '/');
|
2018-12-26 21:48:13 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
Server.prototype.checkId = function (bookId, callback = () => {}) {
|
2018-12-26 23:03:21 +01:00
|
|
|
const bookDataPath = path.resolve(this.fileLocation, bookId + '.json');
|
2018-12-18 21:18:06 +01:00
|
|
|
if (fs.existsSync(bookDataPath)) {
|
|
|
|
const bookDataRaw = fs.readFileSync(bookDataPath);
|
|
|
|
if (bookDataRaw) {
|
|
|
|
const bookData = JSON.parse(bookDataRaw);
|
2018-12-27 20:40:03 +01:00
|
|
|
const bookPath = bookData.hasOwnProperty('fileName') ? bookData.fileName : path.resolve(this.fileLocation, bookId + bookData.fileType);
|
2018-12-18 21:18:06 +01:00
|
|
|
if (fs.existsSync(bookPath)) {
|
2018-12-26 21:48:13 +01:00
|
|
|
return callback(bookPath, bookDataPath, bookData);
|
2018-12-18 21:18:06 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-12-26 21:48:13 +01:00
|
|
|
Server.prototype.deleteBooks = function (socketId) {
|
|
|
|
this.takenBooks.forEach(data => {
|
|
|
|
if (data.socketId === socketId) {
|
2018-12-27 20:40:03 +01:00
|
|
|
const check = this.checkId(data.bookId, (bookPath, bookDataPath) => {
|
2018-12-26 21:48:13 +01:00
|
|
|
fs.unlinkSync(bookPath);
|
2019-01-04 00:04:38 +01:00
|
|
|
// console.log('removed ' + bookPath);
|
2018-12-27 22:54:11 +01:00
|
|
|
fs.renameSync(bookDataPath, unusedFilename.sync(path.resolve(this.historyLocation, Date.now() + '.json')));
|
2019-01-04 23:08:41 +01:00
|
|
|
this.removeHistoryBeyondLimit();
|
2018-12-26 21:48:13 +01:00
|
|
|
});
|
2018-12-27 20:40:03 +01:00
|
|
|
if (check === false) {
|
|
|
|
console.log('couldn\'t find data.bookId');
|
|
|
|
}
|
2018-12-26 21:48:13 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
this.takenBooks = this.takenBooks.filter(data => data.socketId === socketId);
|
|
|
|
}
|
|
|
|
|
2019-01-04 23:08:41 +01:00
|
|
|
Server.prototype.removeHistoryBeyondLimit = function () {
|
|
|
|
if (settings.maxHistory > 0) {
|
|
|
|
let files = fs.readdirSync(this.historyLocation).filter(fileName => fileName.includes('.json'))
|
|
|
|
.map(fileName => { // Cache the file data so sorting doesn't need to re-check each file
|
|
|
|
return { name: fileName, time: fs.statSync(path.resolve(this.historyLocation, fileName)).mtime.getTime() };
|
|
|
|
}).sort((a, b) => b.time - a.time).map(v => v.name); // Sort from newest to oldest.
|
|
|
|
if (files.length > settings.maxHistory) {
|
|
|
|
files.slice(settings.maxHistory).forEach(fileName => {
|
|
|
|
const filePath = path.resolve(this.historyLocation, fileName);
|
|
|
|
fs.unlink(filePath, err => {
|
|
|
|
if (err) {
|
|
|
|
console.error(err);
|
|
|
|
} else {
|
|
|
|
console.log('Deleted ' + filePath);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-18 21:18:06 +01:00
|
|
|
Server.prototype.uuid4 = function () {
|
|
|
|
// https://stackoverflow.com/a/2117523
|
|
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
|
|
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
|
|
return v.toString(16);
|
|
|
|
});
|
|
|
|
}
|
2018-12-26 23:03:21 +01:00
|
|
|
|
2019-01-10 23:20:48 +01:00
|
|
|
Server.prototype.start = function () {
|
|
|
|
this.http.listen((process.env.PORT || settings.port), () => {
|
|
|
|
console.log('Started server on port ' + (process.env.PORT || settings.port));
|
|
|
|
});
|
|
|
|
if (this.https) {
|
|
|
|
this.https.listen(443, () => {
|
|
|
|
console.log('Started SSL server on port 443');
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-26 23:03:21 +01:00
|
|
|
const server = new Server();
|
|
|
|
server.start();
|