isundil 2 年 前
コミット
3e3c3ca774
8 ファイル変更389 行追加0 行削除
  1. 32 0
      .gitignore
  2. 35 0
      main.js
  3. 7 0
      package.json
  4. 13 0
      router/mdi.js
  5. 49 0
      src/config.js
  6. 174 0
      src/databaseHelper.js
  7. 79 0
      src/routerUtils.js
  8. 0 0
      static/public/css/style.css

+ 32 - 0
.gitignore

@@ -0,0 +1,32 @@
+# ---> Node
+# Logs
+npm-debug.log*
+
+/config.json
+/memory.sqlite
+
+# Runtime data
+pids
+*.pid
+*.seed
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directory
+# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
+node_modules
+/package-lock.json
+

+ 35 - 0
main.js

@@ -0,0 +1,35 @@
+#!/bin/node
+
+const path = require('path');
+const fs = require('fs');
+const Router = require('node-simple-router');
+const http = require('http');
+const CONFIG = require('./src/config.js');
+const RouterUtils = require('./src/routerUtils.js').RouterUtils;
+
+function App() {
+    this.router = new Router({ static_route: __dirname+"/static/" });
+    const _app = this;
+    this.routerUtils = new RouterUtils(this);
+    this.databaseHelper = require('./src/databaseHelper.js').DatabaseHelper;
+}
+
+App.prototype.init = async function() {
+    const _app = this;
+    require('./router/mdi.js').register(this);
+
+    await this.databaseHelper.init();
+}
+
+App.prototype.clock = function() {
+    setTimeout(() => this.clock(), 10 *60 *1000); // 10 minutes
+}
+
+App.prototype.run = function() {
+    this.clock();
+    http.createServer(this.router).listen(CONFIG.port);
+}
+
+let app = new App();
+app.init().then(() => app.run());
+

+ 7 - 0
package.json

@@ -0,0 +1,7 @@
+{
+  "dependencies": {
+    "mime-types": "^2.1.35",
+    "node-simple-router": "^0.10.2",
+    "sqlite3": "^5.1.4"
+  }
+}

+ 13 - 0
router/mdi.js

@@ -0,0 +1,13 @@
+
+module.exports = { register: app => {
+    app.routerUtils.staticGet(app, "/public/mdi/materialdesignicons.css", './node_modules/@mdi/font/css/materialdesignicons.css');
+    app.routerUtils.staticGet(app, "/public/mdi/materialdesignicons.css.map", './node_modules/@mdi/font/css/materialdesignicons.css.map');
+    app.routerUtils.staticGet(app, "/public/mdi/materialdesignicons.min.css", './node_modules/@mdi/font/css/materialdesignicons.min.css');
+    app.routerUtils.staticGet(app, "/public/mdi/materialdesignicons.min.css.map", './node_modules/@mdi/font/css/materialdesignicons.min.css.map');
+    app.routerUtils.staticGet(app, "/public/scss/materialdesignicons.scss", './node_modules/@mdi/font/scss/materialdesignicons.scss');
+    app.routerUtils.staticGet(app, "/public/fonts/materialdesignicons-webfont.eot", './node_modules/@mdi/font/fonts/materialdesignicons-webfont.eot');
+    app.routerUtils.staticGet(app, "/public/fonts/materialdesignicons-webfont.ttf", './node_modules/@mdi/font/fonts/materialdesignicons-webfont.ttf');
+    app.routerUtils.staticGet(app, "/public/fonts/materialdesignicons-webfont.woff", './node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff');
+    app.routerUtils.staticGet(app, "/public/fonts/materialdesignicons-webfont.woff2", './node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff2');
+}};
+

+ 49 - 0
src/config.js

@@ -0,0 +1,49 @@
+
+const FILENAME = process.mainModule.path + "/config.json";
+const fs = require('fs');
+const path = require('path');
+
+function validNumber(input) {
+    return !Number.isNaN(Number.parseInt(input));
+}
+
+function validNotEmptyString(input) {
+    return !!input && (""+input).length;
+}
+
+function pickConfig(defaultConfig, configContent) {
+    let configEntries = {};
+    for (let i in defaultConfig) {
+        if (configContent[i] === undefined)
+            configEntries[i] = defaultConfig[i].value;
+        else
+            configEntries[i] = configContent[i];
+        if (!defaultConfig[i].valid(configEntries[i])) {
+            console.error(`Invalid entry in configuration for ${i}: Found ${configEntries[i]}`);
+            hasErrors = true;
+        }
+    }
+    return configEntries;
+}
+
+let hasErrors = false;
+let configEntries = {};
+(() => {
+    let configFile = (process?.argv?.[2] || FILENAME);
+    console.log("CONFIG: Loading configuration from "+configFile);
+    let configContent = JSON.parse(fs.readFileSync(configFile));
+    let defaultConfig = {
+        port: { value: 80, valid: validNumber },
+        database: { value: "", valid: validNotEmptyString },
+        sitename: { value: "Archives", valid: validNotEmptyString }
+    };
+
+    configEntries = pickConfig(defaultConfig, configContent);
+
+    console.log(configEntries);
+    if (hasErrors)
+        throw "Errors found while parsing configuration";
+})();
+
+module.exports = configEntries;
+

+ 174 - 0
src/databaseHelper.js

@@ -0,0 +1,174 @@
+
+const sqlite3 = require('sqlite3');
+const CONFIG = require('./config.js');
+
+function DatabaseHelper() {
+    this.db = null;
+}
+
+DatabaseHelper.prototype.init = function() {
+    return new Promise((ok, ko) => {
+        this.db = new sqlite3.Database(CONFIG.database, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE | sqlite3.OPEN_FULLMUTEX, async (err) => {
+            if (err) {
+                ko(err);
+                return;
+            }
+            let types = [];
+            for (let i =0; i < types.length; ++i) {
+                let instance = new types[i]();
+                await instance.createOrUpdateBase(this);
+            }
+            console.log("Database is ready");
+            ok();
+        });
+    });
+}
+
+DatabaseHelper.prototype.runSql = function(sqlStatement, args) {
+    console.log("DatabaseHelper::runSql ", sqlStatement, args);
+    return new Promise((ok, ko) => {
+        if (sqlStatement.indexOf('--') >= 0) {
+            console.error("Rejecting SQL request containing comments");
+            ko("SQL Comment Error");
+            return;
+        }
+        try {
+            this.db.all(sqlStatement, args, (err, data) => {
+                if (err) {
+                    console.error(err);
+                    ko(err);
+                } else {
+                    ok(data);
+                }
+            });
+        } catch (err) {
+            console.error(err);
+            ko(err);
+        }
+    });
+}
+
+DatabaseHelper.prototype.insertOne = async function(object) {
+    let columns = [];
+    let args = [];
+
+    for (let [key, val] of Object.entries(object.describe())) {
+        columns.push("`"+key+"`");
+        args.push(val);
+    }
+    let request = "insert into `" +object.getTableName()
+            +"` (" +columns.join(", ")
+            +") VALUES (" +columns.map(i => "?").join(",")+")";
+    await this.runSql(request, args);
+}
+
+DatabaseHelper.prototype.upsertOne = async function(obj) {
+    let columns = [];
+    let columnsVal = [];
+    let args = [];
+    let i =0;
+
+    for (let [key, val] of Object.entries(obj.describe())) {
+        columns.push("`"+key+"`");
+        columnsVal.push("`"+key+"`=:"+(i++));
+        args.push(val);
+    }
+    let request = "insert into `" +obj.getTableName()
+            +"` (" +columns.join(", ")
+            +") VALUES (" +columns.map((_, i) => `:${i}`).join(",")+")"
+            +" ON CONFLICT DO UPDATE SET " +columnsVal.join(", ");
+    await this.runSql(request, args);
+}
+
+DatabaseHelper.prototype.buildWhere = function(whereObj, columns, args) {
+    columns = columns || [];
+    args = args || [];
+    if (!whereObj) {
+        columns.push("1=1");
+    } else {
+        for (let [key, val] of Object.entries(whereObj)) {
+            if (val === null) {
+                columns.push("`"+key+"` is null");
+            } else {
+                columns.push("`"+key+"`=?");
+                args.push(val);
+            }
+        }
+    }
+    return {
+        columns: columns,
+        args: args
+    };
+}
+
+DatabaseHelper.prototype.maxVersion = async function(instanceName) {
+    let queryParts = [];
+    for (let i of [ SessionModel, ArchiveModel, TaskModel ])
+        queryParts.push("select max(" +i.prototype.versionColumn.call(null) +") as v from " +i.prototype.getTableName.call(null) +" where instance=:0");
+    let version = await this.runSql("select max(v) as v from (" +queryParts.join(" union ") +")", [ instanceName ]);
+    return (version?.[0]?.v) || 0;
+}
+
+DatabaseHelper.prototype.versionFetch = async function(objectPrototype, minVersion) {
+    let query = "select * from `" +objectPrototype.prototype.getTableName.call(null) +"` where `" +objectPrototype.prototype.versionColumn.call(null)+"` > ? and instance=?";
+    let result = [];
+    for (let i of await this.runSql(query, [minVersion, CONFIG.instanceHostname]))
+    {
+        let resultObj = new objectPrototype();
+        resultObj.fromDb(i);
+        result.push(resultObj);
+    }
+    return result;
+}
+
+DatabaseHelper.prototype.fetch = async function(objectPrototype, where, orderBy) {
+    let whereArgs = this.buildWhere(where);
+    let query = "select * from `" +objectPrototype.prototype.getTableName.call(null) +"` where " +whereArgs.columns.join(" and ");
+    if (orderBy)
+        query += " ORDER BY " +Object.keys(orderBy || {}).map(i => "`"+i+"` " +(orderBy[i] === 'DESC' ? "DESC":"ASC")).join(",");
+    let result = await this.runSql(query, whereArgs.args);
+    let resultArr = [];
+    for (let i of result)
+    {
+        let resultObj = new objectPrototype();
+        resultObj.fromDb(i);
+        resultArr.push(resultObj);
+    }
+    return resultArr;
+}
+
+DatabaseHelper.prototype.findOne = async function(objectPrototype, where) {
+    let whereArgs = this.buildWhere(where);
+    let result = await this.runSql("select * from `" +objectPrototype.prototype.getTableName.call(null) +"` where " +whereArgs.columns.join(" and "), whereArgs.args);
+    if (result && result.length)
+    {
+        let resultObj = new objectPrototype();
+        resultObj.fromDb(result[0]);
+        return resultObj;
+    }
+    return null;
+}
+
+DatabaseHelper.prototype.update = async function(where, object) {
+    if (object.lastUpdated)
+        object.lastUpdated = new Date();
+    await this.rawUpdate(object, where, object.describe());
+}
+
+DatabaseHelper.prototype.rawUpdate = async function(model, where, values) {
+    let columns = [];
+    let args = [];
+
+    for (let [key, val] of Object.entries(values)) {
+        columns.push("`"+key+"`=?");
+        args.push(val);
+    }
+    let whereArgs = this.buildWhere(where, [], args);
+    let query = "update `" +(model.prototype?.getTableName ? model.prototype.getTableName() : model.getTableName())
+            +"` set " +columns.join(", ")
+            +" WHERE " +whereArgs.columns.join(" and ");
+    await this.runSql(query, args);
+}
+
+module.exports.DatabaseHelper = new DatabaseHelper();
+

+ 79 - 0
src/routerUtils.js

@@ -0,0 +1,79 @@
+
+const mime = require('mime-types');
+const path = require('path');
+const fs = require('fs');
+const CONFIG = require('./config.js');
+
+function RouterUtils(app) {
+    this.app = app;
+}
+
+RouterUtils.prototype.httpResponse = function(res, code, response) {
+    res.writeHead(code);
+    res.end(response);
+    return true;
+}
+
+RouterUtils.prototype.redirect = function(res, url) {
+    res.writeHead(302, { Location: url });
+    res.end();
+}
+
+RouterUtils.prototype.apiError = function(res) {
+    res.writeHead(400, { "Content-Type": "application/json"});
+    res.end();
+}
+
+RouterUtils.prototype.jsonResponse = function(res, data) {
+    res.writeHead(200, { "Content-Type": "application/json"});
+    if (typeof data !== 'string')
+        data = JSON.stringify(data);
+    res.end(data);
+}
+
+RouterUtils.prototype.onPageNotFound = function(res) {
+    return this.httpResponse(res, 404, "Page not found...");
+}
+
+RouterUtils.prototype.staticServe = async function(res, filePath) {
+    return new Promise((ok, ko) => {
+        const stream = fs.createReadStream(filePath);
+        const fileSize = fs.statSync(filePath)?.size || undefined;
+        if (!stream || !fileSize) {
+            console.error("RouterUtils::staticGet", url, err);
+            this.httpResponse(res, 500, "Internal Server Error");
+            return ko(err);
+        }
+        res.writeHead(200, {
+            "Content-Type": mime.contentType(path.basename(filePath)),
+            "Content-Length": fileSize
+        });
+        stream.pipe(res);
+        stream.once('end', () => ok());
+    });
+}
+
+RouterUtils.prototype.staticGet = function(app, url, staticResources) {
+    app.router.get(url, (req, res) => {
+        app.routerUtils.staticServe(res, staticResources);
+    });
+}
+
+RouterUtils.prototype.commonRenderInfos = function(endpointName) {
+    let context = {
+        page_title: CONFIG.sitename,
+        endpoints: [],
+        currentEndpoint: endpointName
+    };
+    for (let endpoint in CONFIG.endpoints)
+        context.endpoints.push({
+            name: endpoint,
+            address: '/dashboard/'+this.encodeUrlComponent(endpoint),
+            selected: endpoint === endpointName,
+            icon: CONFIG.endpoints[endpoint].icon
+        });
+    return context;
+}
+
+module.exports = { RouterUtils: RouterUtils, encodeUrlComponent: RouterUtils.encodeUrlComponent, decodeUrlComponent: RouterUtils.decodeUrlComponent };
+

+ 0 - 0
static/public/css/style.css