* Added ability to serve static files from web server

* Web server can have custom error pages, e.g. 404.html
* "file_area" stuff -> "file_base"
* Fix some rare bugs in theme/art loading
* Adjust tab order dynamically for file upload details
This commit is contained in:
Bryan Ashby 2017-02-04 09:20:36 -07:00
parent ff64a7aed5
commit 92772eb1a9
14 changed files with 165 additions and 48 deletions

49
UPGRADE.TXT Normal file
View File

@ -0,0 +1,49 @@
INTRODUCTION
-------------------------------------------------------------------------------
This document covers basic upgrade notes for major ENiGMA½.
BEFORE UPGRADING
-------------------------------------------------------------------------------
* Always back ALL files in the 'db' directory
* Back up your menu.hjson (or renamed equivalent)
GENERAL NOTES
-------------------------------------------------------------------------------
Upgrades often come with changes to the default menu.hjson. It is wise to
use a *different* file name for your BBS's version of this file and point to
it via config.hjson. For example:
general: {
menuFile: my_bbs.hjson
}
After updating code, use a program such as DiffMerge to merge in updates to
my_bbs.hjson from the shipping menu.hjson.
FROM GITHUB
-------------------------------------------------------------------------------
Upgrading from GitHub is easy:
cd /path/to/enigma-bbs
git pull
npm install
PROBLEMS
-------------------------------------------------------------------------------
Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or
file a issue on GitHub.
0.0.1-alpha to 0.0.4-alpha
-------------------------------------------------------------------------------
* Manual Database Upgrade
sqlite3 db/message.sqlite
INSERT INTO message_fts(message_fts) VALUES('rebuild');
* Archiver Changes
If you have overridden or made additions to archivers in your config.hjson
you will need to update them. See docs/archive.md and core/config.js

View File

@ -78,7 +78,7 @@ function getArtFromPath(path, options, cb) {
return iconv.decode(data, encoding); return iconv.decode(data, encoding);
} else { } else {
const eofMarker = defaultEofFromExtension(ext); const eofMarker = defaultEofFromExtension(ext);
return iconv.decode(sliceAtEOF(data, eofMarker), encoding); return iconv.decode(eofMarker ? sliceAtEOF(data, eofMarker) : data, encoding);
} }
} }
@ -213,11 +213,15 @@ function getArt(name, options, cb) {
} }
function defaultEncodingFromExtension(ext) { function defaultEncodingFromExtension(ext) {
return SUPPORTED_ART_TYPES[ext.toLowerCase()].defaultEncoding; const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()];
return artType ? artType.defaultEncoding : 'utf8';
} }
function defaultEofFromExtension(ext) { function defaultEofFromExtension(ext) {
return SUPPORTED_ART_TYPES[ext.toLowerCase()].eof; const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()];
if(artType) {
return artType.eof;
}
} }
// :TODO: Implement the following // :TODO: Implement the following

View File

@ -224,6 +224,8 @@ function getDefaultConfig() {
web : { web : {
domain : 'another-fine-enigma-bbs.org', domain : 'another-fine-enigma-bbs.org',
staticRoot : paths.join(__dirname, './../www'),
http : { http : {
enabled : false, enabled : false,
port : 8080, port : 8080,
@ -364,6 +366,9 @@ function getDefaultConfig() {
}, },
fileTransferProtocols : { fileTransferProtocols : {
//
// See http://www.synchro.net/docs/sexyz.txt for information on SEXYZ
//
zmodem8kSexyz : { zmodem8kSexyz : {
name : 'ZModem 8k (SEXYZ)', name : 'ZModem 8k (SEXYZ)',
type : 'external', type : 'external',

View File

@ -41,15 +41,15 @@ class FileAreaWebAccess {
return self.load(callback); return self.load(callback);
}, },
function addWebRoute(callback) { function addWebRoute(callback) {
const webServer = getServer(WEB_SERVER_PACKAGE_NAME); self.webServer = getServer(WEB_SERVER_PACKAGE_NAME);
if(!webServer) { if(!self.webServer) {
return callback(Errors.DoesNotExist(`Server with package name "${WEB_SERVER_PACKAGE_NAME}" does not exist`)); return callback(Errors.DoesNotExist(`Server with package name "${WEB_SERVER_PACKAGE_NAME}" does not exist`));
} }
const routeAdded = webServer.instance.addRoute({ const routeAdded = self.webServer.instance.addRoute({
method : 'GET', method : 'GET',
path : '/f/[a-zA-Z0-9]+$', // :TODO: allow this to be configurable path : '/f/[a-zA-Z0-9]+$', // :TODO: allow this to be configurable
handler : self.routeWebRequest.bind(self), handler : self.routeWebRequestForFile.bind(self),
}); });
return callback(routeAdded ? null : Errors.General('Failed adding route')); return callback(routeAdded ? null : Errors.General('Failed adding route'));
@ -217,13 +217,10 @@ class FileAreaWebAccess {
} }
fileNotFound(resp) { fileNotFound(resp) {
resp.writeHead(404, { 'Content-Type' : 'text/html' } ); this.webServer.instance.respondWithError(resp, 404, 'File not found.', 'File Not Found');
// :TODO: allow custom 404 - mods/<theme>/file_area_web-404.html
return resp.end('<html><body>Not found</html>');
} }
routeWebRequest(req, resp) { routeWebRequestForFile(req, resp) {
const hashId = paths.basename(req.url); const hashId = paths.basename(req.url);
this.loadServedHashId(hashId, (err, servedItem) => { this.loadServedHashId(hashId, (err, servedItem) => {
@ -259,7 +256,7 @@ class FileAreaWebAccess {
}); });
const headers = { const headers = {
'Content-Type' : mimeTypes.contentType(paths.extname(filePath)) || mimeTypes.contentType('.bin'), 'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'),
'Content-Length' : stats.size, 'Content-Length' : stats.size,
'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`, 'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`,
}; };

View File

@ -25,11 +25,19 @@ const SYSTEM_EOL = require('os').EOL;
const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc. const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc.
/* /*
Notes
-----------------------------------------------------------------------------
See core/config.js for external protocol configuration
Resources Resources
-----------------------------------------------------------------------------
ZModem ZModem
* http://gallium.inria.fr/~doligez/zmodem/zmodem.txt * http://gallium.inria.fr/~doligez/zmodem/zmodem.txt
* https://github.com/protomouse/synchronet/blob/master/src/sbbs3/zmodem.c * https://github.com/protomouse/synchronet/blob/master/src/sbbs3/zmodem.c
*/ */
exports.moduleInfo = { exports.moduleInfo = {

View File

@ -6,7 +6,6 @@ const logger = require('./logger.js');
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash');
const listeningServers = {}; // packageName -> info const listeningServers = {}; // packageName -> info

View File

@ -11,6 +11,8 @@ const http = require('http');
const https = require('https'); const https = require('https');
const _ = require('lodash'); const _ = require('lodash');
const fs = require('fs'); const fs = require('fs');
const paths = require('path');
const mimeTypes = require('mime-types');
const ModuleInfo = exports.moduleInfo = { const ModuleInfo = exports.moduleInfo = {
name : 'Web', name : 'Web',
@ -55,6 +57,14 @@ exports.getModule = class WebServerModule extends ServerModule {
this.enableHttps = Config.contentServers.web.https.enabled || false; this.enableHttps = Config.contentServers.web.https.enabled || false;
this.routes = {}; this.routes = {};
if(Config.contentServers.web.staticRoot) {
this.addRoute({
method : 'GET',
path : '/static/.*$',
handler : this.routeStaticFile,
});
}
} }
createServer() { createServer() {
@ -116,8 +126,54 @@ exports.getModule = class WebServerModule extends ServerModule {
return route ? route.handler(req, resp) : this.accessDenied(resp); return route ? route.handler(req, resp) : this.accessDenied(resp);
} }
respondWithError(resp, code, bodyText, title) {
const customErrorPage = paths.join(Config.contentServers.web.staticRoot, `${code}.html`);
fs.readFile(customErrorPage, 'utf8', (err, data) => {
resp.writeHead(code, { 'Content-Type' : 'text/html' } );
if(err) {
return resp.end(`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>${title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<article>
<h2>${bodyText}</h2>
</article>
</body>
</html>`
);
}
return resp.end(data);
});
}
accessDenied(resp) { accessDenied(resp) {
resp.writeHead(401, { 'Content-Type' : 'text/html' } ); return this.respondWithError(resp, 401, 'Access denied.', 'Access Denied');
return resp.end('<html><body>Access denied</body></html>');
} }
routeStaticFile(req, resp) {
const fileName = req.url.substr(req.url.indexOf('/', 1));
const filePath = paths.join(Config.contentServers.web.staticRoot, fileName);
fs.stat(filePath, (err, stats) => {
if(err) {
return this.respondWithError(resp, 404, 'File not found.', 'File Not Found');
} }
const headers = {
'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'),
'Content-Length' : stats.size,
};
const readStream = fs.createReadStream(filePath);
resp.writeHead(200, headers);
return readStream.pipe(resp);
});
}
};

View File

@ -355,15 +355,15 @@ function getThemeArt(options, cb) {
// //
// options - required: // options - required:
// name // name
// client
// //
// options - optional // options - optional
// client - needed for user's theme/etc.
// themeId // themeId
// asAnsi // asAnsi
// readSauce // readSauce
// random // random
// //
if(!options.themeId && _.has(options.client, 'user.properties.theme_id')) { if(!options.themeId && _.has(options, 'client.user.properties.theme_id')) {
options.themeId = options.client.user.properties.theme_id; options.themeId = options.client.user.properties.theme_id;
} else { } else {
options.themeId = Config.defaults.theme; options.themeId = Config.defaults.theme;
@ -437,9 +437,13 @@ function getThemeArt(options, cb) {
], ],
function complete(err, artInfo) { function complete(err, artInfo) {
if(err) { if(err) {
options.client.log.debug( { error : err }, 'Cannot find art'); if(options.client) {
options.client.log.debug( { error : err.message }, 'Cannot find theme art' );
} else {
Log.debug( { error : err.message }, 'Cannot find theme art' );
} }
cb(err, artInfo); }
return cb(err, artInfo);
} }
); );
} }

View File

@ -4,7 +4,7 @@
// ENiGMA½ // ENiGMA½
const MenuModule = require('../core/menu_module.js').MenuModule; const MenuModule = require('../core/menu_module.js').MenuModule;
const ViewController = require('../core/view_controller.js').ViewController; const ViewController = require('../core/view_controller.js').ViewController;
const getSortedAvailableFileAreas = require('../core/file_area.js').getSortedAvailableFileAreas; const getSortedAvailableFileAreas = require('../core/file_base_area.js').getSortedAvailableFileAreas;
const FileBaseFilters = require('../core/file_base_filter.js'); const FileBaseFilters = require('../core/file_base_filter.js');
const stringFormat = require('../core/string_format.js'); const stringFormat = require('../core/string_format.js');

View File

@ -9,7 +9,7 @@ const theme = require('../core/theme.js');
const FileEntry = require('../core/file_entry.js'); const FileEntry = require('../core/file_entry.js');
const stringFormat = require('../core/string_format.js'); const stringFormat = require('../core/string_format.js');
const createCleanAnsi = require('../core/string_util.js').createCleanAnsi; const createCleanAnsi = require('../core/string_util.js').createCleanAnsi;
const FileArea = require('../core/file_area.js'); const FileArea = require('../core/file_base_area.js');
const Errors = require('../core/enig_error.js').Errors; const Errors = require('../core/enig_error.js').Errors;
const ArchiveUtil = require('../core/archive_util.js'); const ArchiveUtil = require('../core/archive_util.js');
const Config = require('../core/config.js').config; const Config = require('../core/config.js').config;
@ -23,12 +23,6 @@ const cleanControlCodes = require('../core/string_util.js').cleanControlCodes;
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
const paths = require('path');
/*
Misc TODO
*/
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Area List', name : 'File Area List',

View File

@ -4,9 +4,9 @@
// enigma-bbs // enigma-bbs
const MenuModule = require('../core/menu_module.js').MenuModule; const MenuModule = require('../core/menu_module.js').MenuModule;
const stringFormat = require('../core/string_format.js'); const stringFormat = require('../core/string_format.js');
const getSortedAvailableFileAreas = require('../core/file_area.js').getSortedAvailableFileAreas; const getSortedAvailableFileAreas = require('../core/file_base_area.js').getSortedAvailableFileAreas;
const getAreaDefaultStorageDirectory = require('../core/file_area.js').getAreaDefaultStorageDirectory; const getAreaDefaultStorageDirectory = require('../core/file_base_area.js').getAreaDefaultStorageDirectory;
const scanFile = require('../core/file_area.js').scanFile; const scanFile = require('../core/file_base_area.js').scanFile;
const ansiGoto = require('../core/ansi_term.js').goto; const ansiGoto = require('../core/ansi_term.js').goto;
const moveFileWithCollisionHandling = require('../core/file_util.js').moveFileWithCollisionHandling; const moveFileWithCollisionHandling = require('../core/file_util.js').moveFileWithCollisionHandling;
const pathWithTerminatingSeparator = require('../core/file_util.js').pathWithTerminatingSeparator; const pathWithTerminatingSeparator = require('../core/file_util.js').pathWithTerminatingSeparator;
@ -560,20 +560,21 @@ exports.getModule = class UploadModule extends MenuModule {
}, },
function populateViews(callback) { function populateViews(callback) {
const descView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.desc); const descView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.desc);
const tagsView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.tags);
const yearView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.estYear);
tagsView.setText( Array.from(fileEntry.hashTags).join(',') ); // :TODO: optional 'hashTagsSep' like file list/browse
yearView.setText(fileEntry.meta.est_release_year || '');
if(self.fileEntryHasDetectedDesc(fileEntry)) { if(self.fileEntryHasDetectedDesc(fileEntry)) {
descView.setText(fileEntry.desc); descView.setText(fileEntry.desc);
descView.setPropertyValue('mode', 'preview'); descView.setPropertyValue('mode', 'preview');
self.viewControllers.fileDetails.switchFocus(MciViewIds.fileDetails.tags);
// :TODO: it would be nice to take this out of the focus order descView.acceptsFocus = false;
} else {
self.viewControllers.fileDetails.switchFocus(MciViewIds.fileDetails.desc);
} }
const tagsView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.tags);
tagsView.setText( Array.from(fileEntry.hashTags).join(',') ); // :TODO: optional 'hashTagsSep' like file list/browse
const yearView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.estYear);
yearView.setText(fileEntry.meta.est_release_year || '');
return callback(null); return callback(null);
} }
], ],

View File

@ -442,7 +442,7 @@ function fileAreaScan() {
return initConfigAndDatabases(callback); return initConfigAndDatabases(callback);
}, },
function getFileArea(callback) { function getFileArea(callback) {
const fileAreaMod = require('./core/file_area.js'); const fileAreaMod = require('./core/file_base_area.js');
const areaInfo = fileAreaMod.getFileAreaByTag(argv.scan); const areaInfo = fileAreaMod.getFileAreaByTag(argv.scan);
if(!areaInfo) { if(!areaInfo) {

0
www/.gitkeep Normal file
View File