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' ) ;
2018-12-18 21:18:06 +01:00
const socketio = require ( 'socket.io' ) ;
const helmet = require ( 'helmet' ) ;
2018-12-26 21:48:13 +01:00
const bodyParser = require ( 'body-parser' ) ;
const fileUpload = require ( 'express-fileupload' ) ;
const filenamify = require ( 'filenamify' ) ;
const unusedFilename = require ( 'unused-filename' ) ;
2018-12-27 23:47:06 +01:00
const striptags = require ( 'striptags' ) ;
2018-12-27 18:55:59 +01:00
const snarkdown = require ( 'snarkdown' ) ;
2018-12-27 22:54:11 +01:00
const fecha = require ( 'fecha' ) ;
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 ;
2018-12-18 21:18:06 +01:00
this . io = socketio ( this . http ) ;
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 = [ ] ;
2018-12-18 21:18:06 +01:00
this . server . use ( helmet ( ) ) ;
2018-12-26 21:48:13 +01:00
this . server . use ( bodyParser . json ( ) ) ; // support json encoded bodies
this . server . use ( bodyParser . urlencoded ( { extended : true } ) ) ; // support encoded bodies
2019-01-04 21:46:41 +01:00
this . server . use ( '/give' , fileUpload ( { // support file uploads
2018-12-26 21:48:13 +01:00
limits : {
fileSize : ( settings . maxFileSize > 0 ? settings . maxFileSize * 1024 * 1024 : Infinity ) , // filesize in bytes (settings accepts MB)
} ,
} ) ) ;
2019-01-04 21:46:41 +01:00
this . server . use ( '/backup' , fileUpload ( ) ) ; // Allow file upload on backup with no limits.
2018-12-26 21:48:13 +01:00
2018-12-27 20:40:03 +01:00
this . server . use ( '/files' , express . static ( path . join ( _ _dirname , './public/files/' ) ) ) ;
2018-12-27 22:54:11 +01:00
this . server . use ( '/css' , express . static ( path . resolve ( './node_modules/bulma/css/' ) ) ) ;
this . server . use ( '/css' , express . static ( path . join ( _ _dirname , './public/css/' ) ) ) ;
2018-12-26 23:03:21 +01:00
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/' ) ) ) ;
2018-12-18 21:18:06 +01:00
2019-01-07 19:22:27 +01:00
// If a `.well-known` directory exists, allow it to be used for things like Let's Encrypt challenges
if ( fs . existsSync ( path . resolve ( './.well-known' ) ) ) {
this . server . use ( '/.well-known' , express . static ( path . resolve ( './.well-known' ) ) ) ;
}
2019-01-07 19:47:15 +01:00
if ( this . https && settings . forceHTTPS ) {
this . server . use ( function ( req , res , next ) {
if ( ! req . secure ) {
return res . redirect ( [ 'https://' , req . get ( 'Host' ) , req . baseUrl ] . join ( '' ) ) ;
}
next ( ) ;
} ) ;
}
2019-01-07 19:22:27 +01:00
2018-12-18 21:18:06 +01:00
this . server . get ( '/' , ( req , res ) => {
2018-12-27 23:47:06 +01:00
const html = this . generateHomePage ( req ) ;
2018-12-26 23:03:21 +01:00
if ( html ) {
res . send ( html ) ;
} else {
res . send ( 'Something went wrong!' ) ;
}
2018-12-18 21:18:06 +01:00
} ) ;
2018-12-27 18:55:59 +01:00
this . server . get ( '/give' , ( req , res ) => {
2018-12-27 23:47:06 +01:00
const resourcePath = ( req . url . substr ( - 1 ) === '/' ? '../' : './' ) ;
2019-01-04 23:08:41 +01:00
let body = this . fillTemplate ( './templates/pages/uploadForm.html' , { resourcePath } ) ;
body = this . replaceBodyWithTooManyBooksWarning ( body ) ;
2018-12-27 23:47:06 +01:00
const html = this . fillTemplate ( './templates/htmlContainer.html' , { title : 'Give a Book' , resourcePath , body } ) ;
2018-12-27 18:55:59 +01:00
res . send ( html ) ;
} ) ;
2018-12-26 21:48:13 +01:00
this . server . post ( '/give' , ( req , res ) => {
2018-12-27 23:47:06 +01:00
const resourcePath = ( req . url . substr ( - 1 ) === '/' ? '../' : './' ) ;
2018-12-28 00:13:48 +01:00
const { title , author , summary , contributor } = req . body ;
2018-12-27 00:36:35 +01:00
if ( Object . keys ( req . files ) . length > 0
2018-12-28 00:13:48 +01:00
&& req . body . hasOwnProperty ( 'title' ) && title . trim ( ) !== ''
&& req . body . hasOwnProperty ( 'summary' ) && summary . trim ( ) !== '' ) {
2018-12-26 21:48:13 +01:00
const { book } = req . files ;
const fileType = book . name . substr ( book . name . lastIndexOf ( '.' ) ) ;
2018-12-27 18:55:59 +01:00
this . addBook ( { book , title , author , summary , contributor , fileType } , ( ) => {
2018-12-27 00:36:35 +01:00
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' , {
2018-12-27 07:36:47 +01:00
isActive : 'is-active' ,
2018-12-27 00:36:35 +01:00
content : messageBox ,
} ) ;
2019-01-04 23:08:41 +01:00
let body = this . fillTemplate ( './templates/pages/uploadForm.html' , { resourcePath } ) ;
body = this . replaceBodyWithTooManyBooksWarning ( body ) ;
2018-12-27 23:47:06 +01:00
const html = this . fillTemplate ( './templates/htmlContainer.html' , { title : 'Give a Book' , resourcePath , body , modal } ) ;
2018-12-27 00:36:35 +01:00
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' , {
2018-12-27 07:36:47 +01:00
isActive : 'is-active' ,
2018-12-27 00:36:35 +01:00
content : messageBox ,
} ) ;
2019-01-04 23:08:41 +01:00
let body = this . fillTemplate ( './templates/pages/uploadForm.html' , { resourcePath , title , author , summary , contributor } ) ;
body = this . replaceBodyWithTooManyBooksWarning ( body ) ;
2018-12-27 23:47:06 +01:00
const html = this . fillTemplate ( './templates/htmlContainer.html' , { title : 'Give a Book' , resourcePath , body , modal } ) ;
2018-12-27 00:36:35 +01:00
res . send ( html ) ;
2018-12-26 23:03:21 +01:00
} ) ;
2018-12-27 00:36:35 +01:00
} 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 ? '<br>' : '' ) + 'You have not written a title.' ;
}
if ( ! req . body . hasOwnProperty ( 'summary' ) || req . body . summary . trim ( ) === '' ) {
errorMessage += ( errorMessage . length > 0 ? '<br>' : '' ) + 'You have not written a summary.' ;
}
const message = this . fillTemplate ( './templates/elements/messageBox.html' , {
style : 'is-danger' ,
header : 'Missing Required Fields' ,
message : errorMessage ,
} ) ;
2019-01-04 23:08:41 +01:00
let body = this . fillTemplate ( './templates/pages/uploadForm.html' , { resourcePath , title , author , summary , contributor } ) ;
body = this . replaceBodyWithTooManyBooksWarning ( body ) ;
2018-12-27 23:47:06 +01:00
const html = this . fillTemplate ( './templates/htmlContainer.html' , { title : 'Give a Book' , resourcePath , body , message } ) ;
2018-12-27 00:36:35 +01:00
res . send ( html ) ;
2018-12-26 21:48:13 +01:00
}
} ) ;
2018-12-27 22:54:11 +01:00
this . server . get ( '/history' , ( req , res ) => {
2018-12-27 23:47:06 +01:00
const html = this . generateHistoryPage ( req ) ;
if ( html ) {
res . send ( html ) ;
} else {
res . send ( 'Something went wrong!' ) ;
}
} ) ;
this . server . get ( '/about' , ( req , res ) => {
2019-01-05 01:09:15 +01:00
const resourcePath = ( req . url . substr ( - 1 ) === '/' ? '../' : './' ) ;
const body = this . fillTemplate ( './templates/pages/about.html' , { resourcePath } ) ;
2018-12-27 23:47:06 +01:00
const html = this . fillTemplate ( './templates/htmlContainer.html' , { title : 'About' , body } ) ;
2018-12-27 22:54:11 +01:00
if ( html ) {
res . send ( html ) ;
} else {
res . send ( 'Something went wrong!' ) ;
}
} ) ;
2019-01-04 21:46:41 +01:00
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' ] = '<a download href="' + encodeURI ( './files' + backupLocation ) + '">Download</a> (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' ] = '<p>' + backupType + ' is not a valid backup type.</p>' ;
html = this . fillTemplate ( './templates/pages/backup.html' , templateValues ) ;
res . send ( html ) ;
}
} else {
res . send ( html ) ;
}
} else {
res . status ( 400 ) . send ( ) ;
}
} ) ;
2018-12-18 21:18:06 +01:00
this . io . on ( 'connection' , socket => {
2019-01-04 23:47:44 +01:00
if ( ! settings . hideVisitors ) {
this . connections ++ ;
this . io . emit ( 'update visitors' , this . connections ) ;
}
2018-12-18 21:18:06 +01:00
socket . on ( 'take book' , bookId => {
2018-12-26 21:48:13 +01:00
const fileLocation = this . takeBook ( bookId , socket . id ) ;
if ( fileLocation ) {
console . log ( socket . id + ' removed ' + bookId ) ;
const downloadLocation = fileLocation . substr ( fileLocation . lastIndexOf ( '/' ) ) ;
2018-12-27 20:40:03 +01:00
socket . emit ( 'get book' , encodeURI ( './files' + downloadLocation ) ) ;
2019-01-04 00:04:38 +01:00
socket . broadcast . emit ( 'remove book' , bookId ) ;
2018-12-18 21:18:06 +01:00
}
} ) ;
socket . on ( 'disconnect' , ( ) => {
2019-01-04 23:47:44 +01:00
if ( ! settings . hideVisitors ) {
this . connections -- ;
this . io . emit ( 'update visitors' , this . connections ) ;
}
2018-12-26 21:48:13 +01:00
this . deleteBooks ( socket . id ) ;
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 {
data = fs . readFileSync ( path . join ( _ _dirname , file ) , 'utf8' ) ;
}
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 23:47:06 +01:00
Server . prototype . generateHomePage = function ( req ) {
2019-01-04 23:08:41 +01:00
const files = fs . readdirSync ( this . fileLocation ) . 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 . fileLocation , fileName ) ) . mtime . getTime ( ) } ;
} ) . sort ( ( a , b ) => a . time - b . time ) . map ( v => v . name ) ; // Sort from oldest to newest.
2018-12-28 00:33:40 +01:00
let books = files . map ( fileName => {
2018-12-27 18:55:59 +01:00
const bookData = JSON . parse ( fs . readFileSync ( path . resolve ( this . fileLocation , fileName ) , 'utf8' ) ) ;
2018-12-27 20:40:03 +01:00
if ( bookData . hasOwnProperty ( 'fileName' ) ) return '' ;
2019-01-04 00:51:10 +01:00
bookData . author = bookData . author ? bookData . author : '<em>author not provided</em>' ;
bookData . contributor = bookData . contributor ? bookData . contributor : 'Anonymous' ;
2018-12-27 20:40:03 +01:00
2018-12-27 18:55:59 +01:00
const id = fileName . replace ( '.json' , '' ) ;
2018-12-27 22:54:11 +01:00
const confirmId = 'confirm_' + id ;
2018-12-27 23:47:06 +01:00
const added = fecha . format ( new Date ( bookData . added ) , 'hh:mm:ssA on dddd MMMM Do, YYYY' ) ;
2018-12-27 18:55:59 +01:00
const modal = this . fillTemplate ( './templates/elements/modalCard.html' , {
id ,
2018-12-27 22:54:11 +01:00
header : '<h2 class="title">' + bookData . title + '</h2><h4 class="subtitle">' + bookData . author + '</h4>' ,
content : this . fillTemplate ( './templates/elements/bookInfo.html' , {
contributor : bookData . contributor ,
2018-12-27 23:47:06 +01:00
fileFormat : bookData . fileType ,
2018-12-27 22:54:11 +01:00
added ,
summary : snarkdown ( bookData . summary ) ,
} )
+ this . fillTemplate ( './templates/elements/modal.html' , {
id : confirmId ,
content : this . fillTemplate ( './templates/elements/messageBox.html' , {
header : 'Download Your Book' ,
message : this . fillTemplate ( './templates/elements/takeConfirm.html' , { id } ) ,
} ) ,
} ) ,
footer : '<a class="button close">Close</a> <a class="button is-success modal-button" data-modal="' + confirmId + '">Take Book</a>' ,
2018-12-27 18:55:59 +01:00
} ) ;
return this . fillTemplate ( './templates/elements/book.html' , {
id ,
title : bookData . title ,
author : bookData . author ,
2018-12-27 20:40:03 +01:00
fileType : bookData . fileType ,
2018-12-27 18:55:59 +01:00
modal ,
} ) ;
} ) . join ( '' ) ;
2018-12-28 00:33:40 +01:00
if ( books == '' ) {
books = '<div class="column"><div class="content">The shelf is empty. Would you like to <a href="/give">add a book</a>?</div></div>' ;
}
2018-12-28 01:21:42 +01:00
const body = '<h2 class="title">Available Books</h2><div class="columns is-multiline">' + books + '</div>' ;
2018-12-27 23:47:06 +01:00
return this . fillTemplate ( './templates/htmlContainer.html' , {
title : 'View' ,
resourcePath : ( req . url . substr ( - 1 ) === '/' ? '../' : './' ) ,
body
} ) ;
2018-12-27 18:55:59 +01:00
}
2018-12-27 23:47:06 +01:00
Server . prototype . generateHistoryPage = function ( req ) {
2019-01-04 23:08:41 +01:00
const 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.
2018-12-28 00:33:40 +01:00
let history = files . map ( fileName => {
2018-12-27 22:54:11 +01:00
const bookData = JSON . parse ( fs . readFileSync ( path . resolve ( this . historyLocation , fileName ) , 'utf8' ) ) ;
2019-01-04 00:51:10 +01:00
bookData . author = bookData . author ? bookData . author : '<em>author not provided</em>' ;
bookData . contributor = bookData . contributor ? bookData . contributor : 'Anonymous' ;
2018-12-27 22:54:11 +01:00
const id = fileName . replace ( '.json' , '' ) ;
2018-12-27 23:47:06 +01:00
const added = fecha . format ( new Date ( bookData . added ) , 'hh:mm:ssA on dddd MMMM Do, YYYY' ) ;
const removed = fecha . format ( new Date ( parseInt ( id ) ) , 'hh:mm:ssA on dddd MMMM Do, YYYY' ) ;
2019-01-04 00:04:38 +01:00
const removedTag = '<div class="control"><div class="tags has-addons"><span class="tag">Taken</span><span class="tag is-warning">' + removed + '</span></div></div>' ;
2018-12-27 22:54:11 +01:00
const modal = this . fillTemplate ( './templates/elements/modalCard.html' , {
id ,
header : '<h2 class="title">' + bookData . title + '</h2><h4 class="subtitle">' + bookData . author + '</h4>' ,
content : this . fillTemplate ( './templates/elements/bookInfo.html' , {
contributor : bookData . contributor ,
fileFormat : bookData . fileType ,
added ,
removedTag ,
summary : snarkdown ( bookData . summary ) ,
} ) ,
footer : '<a class="button close">Close</a>' ,
} ) ;
return this . fillTemplate ( './templates/elements/book.html' , {
id ,
title : bookData . title ,
author : bookData . author ,
fileType : bookData . fileType ,
modal ,
} ) ;
} ) . join ( '' ) ;
2018-12-28 00:33:40 +01:00
if ( history == '' ) {
history = '<div class="column"><div class="content">No books have been taken yet. Would you like to <a href="/">take a book</a>?</div></div>' ;
}
2018-12-28 01:21:42 +01:00
const body = '<h2 class="title">History</h2><div class="columns is-multiline">' + history + '</div>' ;
2018-12-27 23:47:06 +01:00
return this . fillTemplate ( './templates/htmlContainer.html' , {
title : 'History' ,
resourcePath : ( req . url . substr ( - 1 ) === '/' ? '../' : './' ) ,
body
} ) ;
2018-12-27 22:54:11 +01:00
}
2018-12-18 21:18:06 +01:00
Server . prototype . start = function ( ) {
2019-01-05 01:41:13 +01:00
this . http . listen ( ( process . env . PORT || settings . port ) , ( ) => {
console . log ( 'Started server on port ' + ( process . env . PORT || settings . port ) ) ;
2018-12-18 21:18:06 +01:00
} ) ;
2019-01-07 19:22:27 +01:00
if ( this . https ) {
this . https . listen ( 443 , ( ) => {
console . log ( 'Started SSL server on port 443' ) ;
} ) ;
}
2018-12-18 21:18:06 +01:00
}
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
const server = new Server ( ) ;
server . start ( ) ;