3 |
5 | {{header}}
4 |
6 | {{message}}
7 |
8 | Originally retrieved from ${bookData.source}
` : ''; 18 | 19 | const id = fileName.replace('.json', ''); 20 | const added = fecha.format(new Date(bookData.added), 'hh:mm:ssA on dddd MMMM Do, YYYY'); 21 | const removed = fecha.format(new Date(parseInt(id)), 'hh:mm:ssA on dddd MMMM Do, YYYY'); 22 | const removedTag = 'Originally retrieved from ${bookData.source}
` : ''; 28 | 29 | const id = fileDetails.name.replace('.json', ''); 30 | const confirmId = 'confirm_' + id; 31 | const added = fecha.format(new Date(bookData.added), 'hh:mm:ssA on dddd MMMM Do, YYYY'); 32 | const modal = app.templater.fill('./templates/elements/modalCard.html', { 33 | id, 34 | header: '' + backupType + ' is not a valid backup type.
'; 51 | html = app.templater.fill('./templates/pages/tools.html', templateValues); 52 | res.send(html); 53 | } 54 | } else { 55 | res.send(html); 56 | } 57 | } else { 58 | res.status(400).send(); 59 | } 60 | }); 61 | } -------------------------------------------------------------------------------- /routes/socketio.js: -------------------------------------------------------------------------------- 1 | const settings = require('../settings.json'); 2 | 3 | module.exports = function (app) { 4 | app.io.on('connection', socket => { 5 | if (!settings.hideVisitors) { 6 | app.connections++; 7 | app.io.emit('update visitors', app.connections); 8 | } 9 | 10 | socket.on('take book', bookId => { 11 | const fileLocation = app.takeBook(bookId, socket.id); 12 | if (fileLocation) { 13 | console.log(socket.id + ' removed ' + bookId); 14 | const downloadLocation = fileLocation.substr(fileLocation.lastIndexOf('/')); 15 | socket.emit('get book', encodeURI('./files' + downloadLocation)); 16 | socket.broadcast.emit('remove book', bookId); 17 | } 18 | }); 19 | 20 | socket.on('disconnect', () => { 21 | if (!settings.hideVisitors) { 22 | app.connections--; 23 | if (app.connections < 0) app.connections = 0; 24 | app.io.emit('update visitors', app.connections); 25 | } 26 | app.deleteBooks(socket.id); 27 | }); 28 | }); 29 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const express = require('express'); 4 | const http = require('http'); 5 | const https = require('https'); 6 | const SocketIoServer = require('socket.io'); 7 | const filenamify = require('filenamify'); 8 | const unusedFilename = require('unused-filename'); 9 | const striptags = require('striptags'); 10 | 11 | const settings = require('./settings.json'); 12 | const privateKey = settings.sslPrivateKey ? fs.readFileSync(settings.sslPrivateKey, 'utf8') : null; 13 | const certificate = settings.sslCertificate ? fs.readFileSync(settings.sslCertificate, 'utf8') : null; 14 | const ca = settings.sslCertificateAuthority ? fs.readFileSync(settings.sslCertificateAuthority, 'utf8') : null; 15 | 16 | const Templater = require('./templates/Templater'); 17 | 18 | function Server () { 19 | this.server = express(); 20 | this.http = http.Server(this.server); 21 | this.https = privateKey && certificate ? https.createServer({ key: privateKey, cert: certificate, ca }, this.server) : null; 22 | this.io = SocketIoServer(); 23 | if (!settings.forceHTTPS) { 24 | this.io.attach(this.http); 25 | } 26 | if (this.https) { 27 | this.io.attach(this.https); 28 | } 29 | 30 | this.fileLocation = path.resolve(settings.fileLocation); 31 | this.historyLocation = path.resolve(settings.historyLocation); 32 | 33 | this.templater = new Templater(this); 34 | 35 | this.connections = 0; 36 | this.takenBooks = []; 37 | 38 | require('./routes/middleware')(this); 39 | 40 | require('./routes/get_home')(this); 41 | 42 | require('./routes/get_give')(this); 43 | require('./routes/post_give')(this); 44 | 45 | require('./routes/get_history')(this); 46 | 47 | require('./routes/get_about')(this); 48 | 49 | require('./routes/get_tools')(this); 50 | require('./routes/post_tools')(this); 51 | 52 | require('./routes/socketio')(this); 53 | } 54 | 55 | Server.prototype.replaceBodyWithTooManyBooksWarning = function (body) { 56 | if (settings.maxLibrarySize > 0) { 57 | const numberOfBooks = fs.readdirSync(this.fileLocation).filter(fileName => fileName.includes('.json')).length; 58 | if (numberOfBooks >= settings.maxLibrarySize) { 59 | body = this.templater.fill('./templates/elements/messageBox.html', { 60 | style: 'is-danger', 61 | title: 'Library Full', 62 | 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.', 63 | }); 64 | } 65 | } 66 | 67 | return body; 68 | } 69 | 70 | Server.prototype.addBook = function (uploadData = {}, success = () => {}, error = () => {}) { 71 | const { book } = uploadData; 72 | 73 | // If the file is too big, error out. 74 | if (book.truncated === true) { 75 | delete book; 76 | return error('The file provided is too big'); 77 | } 78 | 79 | const bookId = this.uuid4(); 80 | const bookPath = path.resolve(this.fileLocation, bookId); 81 | 82 | const bookData = { 83 | title: striptags(uploadData.title.trim()), 84 | author: striptags(uploadData.author.trim()), 85 | summary: striptags(uploadData.summary.trim().replace(/\r\n/g, '\n')), 86 | contributor: striptags(uploadData.contributor.trim()), 87 | source: striptags(uploadData.source.trim()), 88 | added: Date.now(), 89 | fileType: uploadData.fileType, 90 | } 91 | 92 | const bookFilePath = unusedFilename.sync(path.resolve(bookPath + bookData.fileType)); 93 | return book.mv(bookFilePath, function (err) { 94 | if (err) { 95 | console.log(err); 96 | error(err); 97 | } else { 98 | const bookDataPath = unusedFilename.sync(path.resolve(bookPath + '.json')); 99 | fs.writeFileSync(bookDataPath, JSON.stringify(bookData)); 100 | success(); 101 | // console.log('uploaded ' + bookData.title + ' to ' + bookFilePath + ', and saved metadata to ' + bookDataPath); 102 | } 103 | }); 104 | } 105 | 106 | Server.prototype.takeBook = function (bookId, socketId) { 107 | return this.checkId(bookId, (bookPath, bookDataPath, bookData) => { 108 | const bookName = filenamify(bookData.title); 109 | const newFileName = unusedFilename.sync(path.resolve(this.fileLocation, bookName + bookData.fileType)); 110 | bookData.fileName = newFileName; 111 | fs.renameSync(bookPath, newFileName); 112 | fs.writeFileSync(bookDataPath, JSON.stringify(bookData)); 113 | this.takenBooks.push({ socketId, bookId }); 114 | return newFileName.replace(/\\/g, '/'); 115 | }); 116 | } 117 | 118 | Server.prototype.checkId = function (bookId, callback = () => {}) { 119 | const bookDataPath = path.resolve(this.fileLocation, bookId + '.json'); 120 | if (fs.existsSync(bookDataPath)) { 121 | const bookDataRaw = fs.readFileSync(bookDataPath); 122 | if (bookDataRaw) { 123 | const bookData = JSON.parse(bookDataRaw); 124 | const bookPath = bookData.hasOwnProperty('fileName') ? bookData.fileName : path.resolve(this.fileLocation, bookId + bookData.fileType); 125 | if (fs.existsSync(bookPath)) { 126 | return callback(bookPath, bookDataPath, bookData); 127 | } 128 | } 129 | } 130 | 131 | return false; 132 | } 133 | 134 | Server.prototype.deleteBooks = function (socketId) { 135 | this.takenBooks.forEach(data => { 136 | if (data.socketId === socketId) { 137 | const check = this.checkId(data.bookId, (bookPath, bookDataPath) => { 138 | fs.unlinkSync(bookPath); 139 | // console.log('removed ' + bookPath); 140 | fs.renameSync(bookDataPath, unusedFilename.sync(path.resolve(this.historyLocation, Date.now() + '.json'))); 141 | this.removeHistoryBeyondLimit(); 142 | }); 143 | if (check === false) { 144 | console.log('couldn\'t find data.bookId'); 145 | } 146 | } 147 | }); 148 | this.takenBooks = this.takenBooks.filter(data => data.socketId === socketId); 149 | } 150 | 151 | Server.prototype.removeHistoryBeyondLimit = function () { 152 | if (settings.maxHistory > 0) { 153 | let files = fs.readdirSync(this.historyLocation).filter(fileName => fileName.includes('.json')) 154 | .map(fileName => { // Cache the file data so sorting doesn't need to re-check each file 155 | return { name: fileName, time: fs.statSync(path.resolve(this.historyLocation, fileName)).mtime.getTime() }; 156 | }).sort((a, b) => b.time - a.time).map(v => v.name); // Sort from newest to oldest. 157 | if (files.length > settings.maxHistory) { 158 | files.slice(settings.maxHistory).forEach(fileName => { 159 | const filePath = path.resolve(this.historyLocation, fileName); 160 | fs.unlink(filePath, err => { 161 | if (err) { 162 | console.error(err); 163 | } else { 164 | console.log('Deleted ' + filePath); 165 | } 166 | }) 167 | }); 168 | } 169 | } 170 | } 171 | 172 | Server.prototype.uuid4 = function () { 173 | // https://stackoverflow.com/a/2117523 174 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 175 | var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 176 | return v.toString(16); 177 | }); 178 | } 179 | 180 | Server.prototype.start = function () { 181 | this.http.listen((process.env.PORT || settings.port), () => { 182 | console.log('Started server on port ' + (process.env.PORT || settings.port)); 183 | }); 184 | if (this.https) { 185 | this.https.listen(settings.sslPort, () => { 186 | console.log('Started SSL server on port ' + settings.sslPort); 187 | }); 188 | } 189 | } 190 | 191 | const server = new Server(); 192 | server.start(); 193 | -------------------------------------------------------------------------------- /settings.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3000, 3 | "siteTitle": "Little Library", 4 | "titleSeparator": " | ", 5 | "fileLocation": "./public/files/", 6 | "historyLocation": "./public/history/", 7 | "maxLibrarySize": 0, 8 | "maxFileSize": 0, 9 | "maxHistory": 0, 10 | "allowedFormats": [".epub", ".mobi", ".pdf"], 11 | "toolsPassword": "password", 12 | "hideVisitors": false, 13 | "sslPort": 443, 14 | "sslPrivateKey": null, 15 | "sslCertificate": null, 16 | "sslCertificateAuthority": null, 17 | "forceHTTPS": false 18 | } -------------------------------------------------------------------------------- /templates/Templater.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | const settings = require('../settings.json'); 5 | 6 | module.exports = class { 7 | constructor (app) { 8 | this.app = app; 9 | this.cache = {}; 10 | } 11 | 12 | fill (file, templateVars = {}) { 13 | let data; 14 | if (this.cache.hasOwnProperty(file)) { 15 | data = this.cache[file]; 16 | } else { 17 | data = fs.readFileSync(path.resolve(file), 'utf8'); 18 | } 19 | if (data) { 20 | if (!this.cache.hasOwnProperty(file)) { 21 | this.cache[file] = data; 22 | } 23 | 24 | let filledTemplate = data.replace(/\{\{siteTitle\}\}/g, settings.siteTitle) 25 | .replace(/\{\{titleSeparator\}\}/g, settings.titleSeparator) 26 | .replace(/\{\{allowedFormats\}\}/g, settings.allowedFormats.join(',')) 27 | .replace(/\{\{maxFileSize\}\}/g, (settings.maxFileSize > 0 ? settings.maxFileSize + 'MB' : 'no')); 28 | 29 | if (fs.existsSync(path.resolve('./customHtmlAfterFooter.html'))) { 30 | const customHtmlAfterFooter = fs.readFileSync(path.resolve('./customHtmlAfterFooter.html')); 31 | filledTemplate = filledTemplate.replace(/\{\{customHtmlAfterFooter\}\}/g, customHtmlAfterFooter); 32 | } 33 | 34 | for (let templateVar in templateVars) { 35 | const regExp = new RegExp('\{\{' + templateVar + '\}\}', 'g') 36 | filledTemplate = filledTemplate.replace(regExp, templateVars[templateVar]); 37 | } 38 | 39 | // If any template variable is not provided, don't even render them. 40 | filledTemplate = filledTemplate.replace(/\{\{[a-zA-Z0-9\-_]+\}\}/g, ''); 41 | 42 | return filledTemplate; 43 | } 44 | 45 | return data; 46 | } 47 | } -------------------------------------------------------------------------------- /templates/elements/book.html: -------------------------------------------------------------------------------- 1 |{{contributor}} said:
22 | {{summary}} 23 |Please ensure that you're using a device that can download and save the file correctly!
3 |6 | {{siteTitle}} is a digital give a book, take a book website for e-books. Books on this 7 | site are treated as though they were physical, meaning that when someone takes a book, 8 | it becomes unavailable to be downloaded. 9 |
10 |
11 | Books that can be given and are available here are in the following file formats:
12 | {{allowedFormats}}
13 | with {{maxFileSize}} maximum file size.
14 |
18 | You can view the available books on the home page of the site in exactly the way that they were 19 | provided by others. Items listed show the title, author (if provided), and the file type of the 20 | book, and clicking the item will reveal more information about the book, including the contributor 21 | and the contributor's reason for adding the book. 22 |
23 |24 | While browsing, you will notice that there is a number in the top left corner of the website that 25 | shows how many people are currently looking at the library, including you. This number updates in 26 | real time as people come and go. If you are looking at a book and someone takes it, you will be 27 | notified that it was taken and will not be able to take it yourself. Please note that you cannot 28 | see what another person might be looking at, so the same thing happens to others when you take a 29 | book they are looking at—it is not a personal sleight against you if a book is taken. 30 |
31 |32 | You can also browse the history of the shelf and see what books had once been in the library as 33 | well as what the contributor wrote about it. 34 |
35 | 36 |38 | When looking at a book's information, you will notice a "Take Book" button. Clicking that will 39 | allow you to start the download process. 40 |
41 |42 | After confirming that you understand, a new button labeled "Download" link will appear. Click it 43 | to start the download to your device. As soon as you confirm that you understand, the book will 44 | instantly become unavailable to anyone else viewing the library! If you leave or even refresh the 45 | page before downloading the file, it will be unavailable. Once you leave the page after 46 | clicking the "I understand" button, the file is deleted from the server. 47 |
48 |49 | Please be careful, considerate, and responsible so you do not accidentally lose the file forever. 50 |
51 | 52 |54 | When you give a book, in addition to the file itself, you are required to include the title 55 | of the book and your reason for giving it. You are encouraged to include your thoughts about 56 | the book when giving your reason. 57 | While it is not required, you are encouraged to also include the author of the book and your own 58 | name. 59 |
60 |61 | When your book is taken from the shelf, the data that you entered when giving it will remain in 62 | the History listing so that anyone can look at what books 63 | had been on the shelf at one time and see what the contributor thought about it. 64 |
65 |10 | 11 | 12 | {{resetVisitors}} 13 |
14 |
16 | These tools allows you to download a .zip
file of your Files folder and your History
17 | folder or re-import one of the zipped files you received from a backup.
18 |
22 | 23 | 24 | {{filesDownload}} 25 |
26 |27 | 28 | 29 | {{historyDownload}} 30 |
31 | 32 |9 | Use this form to add a book to the library! 10 |
11 |