const path = require('path');
const fs = require('fs');
const express = require('express');
const http = require('http');
const socketio = require('socket.io');
const helmet = require('helmet');
const bodyParser = require('body-parser');
const fileUpload = require('express-fileupload');
const filenamify = require('filenamify');
const unusedFilename = require('unused-filename');
const striptags = require('striptags');
const snarkdown = require('snarkdown');
const fecha = require('fecha');
const settings = require('./settings.json');
function Server () {
this.server = express();
this.http = http.Server(this.server);
this.io = socketio(this.http);
this.fileLocation = path.resolve(settings.fileLocation);
this.historyLocation = path.resolve(settings.historyLocation);
this.templateCache = {};
this.connections = 0;
this.takenBooks = [];
this.server.use(helmet());
this.server.use(bodyParser.json()); // support json encoded bodies
this.server.use(bodyParser.urlencoded({ extended: true })); // support encoded bodies
this.server.use('/give', fileUpload({ // support file uploads
limits: {
fileSize: (settings.maxFileSize > 0 ? settings.maxFileSize * 1024 * 1024 : Infinity), // filesize in bytes (settings accepts MB)
},
}));
this.server.use('/backup', fileUpload()); // Allow file upload on backup with no limits.
this.server.use('/files', express.static(path.join(__dirname, './public/files/')));
this.server.use('/css', express.static(path.resolve('./node_modules/bulma/css/')));
this.server.use('/css', express.static(path.join(__dirname, './public/css/')));
this.server.use('/js', express.static(path.join(__dirname, './public/js/')));
this.server.use('/js', express.static(path.resolve('./node_modules/jquery/dist/')));
this.server.use('/js', express.static(path.resolve('./node_modules/socket.io-client/dist/')));
this.server.get('/', (req, res) => {
const html = this.generateHomePage(req);
if (html) {
res.send(html);
} else {
res.send('Something went wrong!');
}
});
this.server.get('/give', (req, res) => {
const resourcePath = (req.url.substr(-1) === '/' ? '../' : './');
const body = this.fillTemplate('./templates/pages/uploadForm.html', { resourcePath });
const html = this.fillTemplate('./templates/htmlContainer.html', { title: 'Give a Book', resourcePath, body });
res.send(html);
});
this.server.post('/give', (req, res) => {
const resourcePath = (req.url.substr(-1) === '/' ? '../' : './');
const { title, author, summary, contributor } = req.body;
if (Object.keys(req.files).length > 0
&& req.body.hasOwnProperty('title') && title.trim() !== ''
&& req.body.hasOwnProperty('summary') && summary.trim() !== '') {
const { book } = req.files;
const fileType = book.name.substr(book.name.lastIndexOf('.'));
this.addBook({ book, title, author, summary, contributor, fileType }, () => {
const messageBox = this.fillTemplate('./templates/elements/messageBox.html', {
style: 'is-success',
header: 'Upload Successful',
message: 'Thank you for your contribution!'
});
const modal = this.fillTemplate('./templates/elements/modal.html', {
isActive: 'is-active',
content: messageBox,
});
const body = this.fillTemplate('./templates/pages/uploadForm.html', { resourcePath });
const html = this.fillTemplate('./templates/htmlContainer.html', { title: 'Give a Book', resourcePath, body, modal });
res.send(html);
}, (err) => {
const messageBox = this.fillTemplate('./templates/elements/messageBox.html', {
style: 'is-danger',
header: 'Upload Failed',
message: err,
});
const modal = this.fillTemplate('./templates/elements/modal.html', {
isActive: 'is-active',
content: messageBox,
});
const body = this.fillTemplate('./templates/pages/uploadForm.html', { resourcePath, title, author, summary, contributor });
const html = this.fillTemplate('./templates/htmlContainer.html', { title: 'Give a Book', resourcePath, body, modal });
res.send(html);
});
} else {
let errorMessage = '';
if (Object.keys(req.files).length <= 0) {
errorMessage += 'You have not selected a file.';
}
if (!req.body.hasOwnProperty('title') || req.body.title.trim() === '') {
errorMessage += (errorMessage.length > 0 ? '
' : '') + 'You have not written a title.';
}
if (!req.body.hasOwnProperty('summary') || req.body.summary.trim() === '') {
errorMessage += (errorMessage.length > 0 ? '
' : '') + 'You have not written a summary.';
}
const message = this.fillTemplate('./templates/elements/messageBox.html', {
style: 'is-danger',
header: 'Missing Required Fields',
message: errorMessage,
});
const body = this.fillTemplate('./templates/pages/uploadForm.html', { resourcePath, title, author, summary, contributor });
const html = this.fillTemplate('./templates/htmlContainer.html', { title: 'Give a Book', resourcePath, body, message });
res.send(html);
}
});
this.server.get('/history', (req, res) => {
const html = this.generateHistoryPage(req);
if (html) {
res.send(html);
} else {
res.send('Something went wrong!');
}
});
this.server.get('/about', (req, res) => {
const body = this.fillTemplate('./templates/pages/about.html');
const html = this.fillTemplate('./templates/htmlContainer.html', { title: 'About', body });
if (html) {
res.send(html);
} else {
res.send('Something went wrong!');
}
});
this.server.get('/backup', (req, res) => {
if (req.query.pass === settings.backupPassword) {
const templateValues = {};
let html = this.fillTemplate('./templates/pages/backup.html', templateValues);
if (req.query.dl && ['files', 'history'].includes(req.query.dl)) {
const onezip = require('onezip');
const { dl } = req.query;
const saveLocation = path.resolve(this.fileLocation, dl + 'Backup.zip');
const backupLocation = dl === 'history' ? this.historyLocation : this.fileLocation;
const files = fs.readdirSync(backupLocation).filter(fileName => !fileName.includes('.zip'));
onezip.pack(backupLocation, saveLocation, files)
.on('start', () => {
console.info('Starting a backup zip of ' + dl)
})
.on('error', (error) => {
console.error(error);
templateValues[dl + 'Download'] = 'Something went wrong: ' + JSON.stringify(error);
html = this.fillTemplate('./templates/pages/backup.html', templateValues);
res.send(html);
})
.on('end', () => {
console.log('Backup complete. Saved to ' + saveLocation);
let backupLocation = saveLocation.replace(/\\/g, '/');
backupLocation = backupLocation.substr(backupLocation.lastIndexOf('/'));
templateValues[dl + 'Download'] = 'Download (This will be removed from the server in 1 hour)';
html = this.fillTemplate('./templates/pages/backup.html', templateValues);
res.send(html);
console.log('Will delete ' + saveLocation + ' in 1 hour');
setTimeout(() => {
fs.unlink(saveLocation, (err) => {
if (err) {
console.error(err);
} else {
console.log('Deleted backup file ' + saveLocation);
}
})
}, 60 * 60 * 1000);
});
} else {
res.send(html);
}
} else {
res.status(400).send();
}
});
this.server.post('/backup', (req, res) => {
if (req.query.pass === settings.backupPassword) {
const templateValues = {};
let html = this.fillTemplate('./templates/pages/backup.html', templateValues);
const { files } = req;
if (Object.keys(files).length > 0) {
const backupType = Object.keys(files)[0];
if (['files', 'history'].includes(backupType)) {
const onezip = require('onezip');
const uploadPath = path.resolve('./', backupType + 'UploadedBackup.zip');
files[backupType].mv(uploadPath, (err) => {
if (err) {
console.error(error);
templateValues[backupType + 'UploadSuccess'] = 'Could not upload the file.';
html = this.fillTemplate('./templates/pages/backup.html', templateValues);
res.send(html);
} else {
onezip.extract(uploadPath, path.resolve('./public', backupType))
.on('start', () => {
console.info('Extracting file ' + uploadPath)
})
.on('error', (error) => {
console.error(error);
templateValues[backupType + 'UploadSuccess'] = 'Something went wrong: ' + JSON.stringify(error);
html = this.fillTemplate('./templates/pages/backup.html', templateValues);
res.send(html);
})
.on('end', () => {
templateValues[backupType + 'UploadSuccess'] = 'Uploaded Successfully!';
html = this.fillTemplate('./templates/pages/backup.html', templateValues);
res.send(html);
fs.unlink(uploadPath, (err) => {
if (err) {
console.error(err);
} else {
console.log('Deleted backup file ' + uploadPath);
}
})
});
}
});
} else {
templateValues['generalError'] = '
' + backupType + ' is not a valid backup type.
'; html = this.fillTemplate('./templates/pages/backup.html', templateValues); res.send(html); } } else { res.send(html); } } else { res.status(400).send(); } }); this.io.on('connection', socket => { this.connections++; this.io.emit('update visitors', this.connections); socket.on('take book', bookId => { const fileLocation = this.takeBook(bookId, socket.id); if (fileLocation) { console.log(socket.id + ' removed ' + bookId); const downloadLocation = fileLocation.substr(fileLocation.lastIndexOf('/')); socket.emit('get book', encodeURI('./files' + downloadLocation)); socket.broadcast.emit('remove book', bookId); } }); socket.on('disconnect', () => { this.connections--; this.io.emit('update visitors', this.connections); this.deleteBooks(socket.id); }); }); } Server.prototype.fillTemplate = function (file, templateVars = {}) { let data; if (this.templateCache.hasOwnProperty(file)) { data = this.templateCache[file]; } else { data = fs.readFileSync(path.join(__dirname, file), 'utf8'); } if (data) { if (!this.templateCache.hasOwnProperty(file)) { this.templateCache[file] = data; } let filledTemplate = data.replace(/\{\{siteTitle\}\}/g, settings.siteTitle) .replace(/\{\{titleSeparator\}\}/g, settings.titleSeparator) .replace(/\{\{allowedFormats\}\}/g, settings.allowedFormats.join(',')) .replace(/\{\{maxFileSize\}\}/g, (settings.maxFileSize > 0 ? settings.maxFileSize + 'MB' : 'no')); for (let templateVar in templateVars) { const regExp = new RegExp('\{\{' + templateVar + '\}\}', 'g') filledTemplate = filledTemplate.replace(regExp, templateVars[templateVar]); } // If any template variable is not provided, don't even render them. filledTemplate = filledTemplate.replace(/\{\{[a-zA-Z0-9\-_]+\}\}/g, ''); return filledTemplate; } return data; } Server.prototype.generateHomePage = function (req) { const files = fs.readdirSync(this.fileLocation).filter(fileName => fileName.includes('.json')); let books = files.map(fileName => { const bookData = JSON.parse(fs.readFileSync(path.resolve(this.fileLocation, fileName), 'utf8')); if (bookData.hasOwnProperty('fileName')) return ''; bookData.author = bookData.author ? bookData.author : 'author not provided'; bookData.contributor = bookData.contributor ? bookData.contributor : 'Anonymous'; const id = fileName.replace('.json', ''); const confirmId = 'confirm_' + id; const added = fecha.format(new Date(bookData.added), 'hh:mm:ssA on dddd MMMM Do, YYYY'); const modal = this.fillTemplate('./templates/elements/modalCard.html', { id, header: '