127.0.0.1:8000 watch-together / master server / utils / index.js
master

Tree @master (Download .tar.gz)

index.js @masterraw · history · blame

const {
    v4: uuid
} = require('uuid');
const {
    promisify
} = require('util');
const fs = require('fs');
const zxcvbn = require('zxcvbn');
const crypto = require('crypto');
const mysql = require('mysql');
const log = require('./log').create_logger("utils");
const STATUS_CODES = require('../../status.json');
const STATUS_CODES_ENUM = Object.assign({
    video: require('../../video_status.json')
}, enumify(Object.keys(STATUS_CODES)));
const get_folder_size = promisify(require('get-folder-size'));

const max_video_dir_size = 20000000000; // 20 GB

const mysql_options = {
    connectionLimit: 10,
    host: 'localhost',
    user: process.env.SQL_USER,
    password: process.env.SQL_PASSWORD,
    database: 'watch_together'
};

const MYSQL_POOL = mysql.createPool(mysql_options);

function enumify(arr, options) {
    options = Object.assign({
        randomize: false
    }, options || {});

    return arr.reduce(function(acc, val) {
        return Object.assign({
            [val]: options.randomize ? uuid() : (options.mapify ? true : val)
        }, acc);
    }, {});
}

function query(query, wildcards) {
    return new Promise(function(resolve, reject) {
        let args = [query, function(error, rows) {
            if (error) {
                reject(error);
            } else {
                resolve(rows);
            }
        }];

        log.v("Execute query: ", query);
        if (wildcards) {
            args.splice(1, 0, wildcards);
            log.v("With wildcards:", wildcards);
        }

        MYSQL_POOL.query(...args);
    });
}

function not_ok_response(status_code, data) {
    log.v("Respond with status", status_code, "Data", data);
    return Object.assign({
        status: status_code
    }, data || {});
}

function handle_err_status_res(res) {
    return function(error) {
        if (!res.headersSent) {
            log.general.error("Error response wrapper invoked with unknown error", error);
            if (typeof error !== "number" || error < 300) {
                error = 500;
            }
            res.sendStatus(error);
        } else {
            log.general.error("Response already sent in response error handler");
        }
    };
}

function create_error_handler(ext_log, non_fatal, message) {
    message = message || (x => x);
    return function(error) {
        if (ext_log.locals && ext_log.locals.log) {
            // handle passing in Express res object as logger
            ext_log = ext_log.locals.log;
        }
        ext_log.general.error(error.stack);

        if (non_fatal) {
            return new Promise(r => r(non_fatal));
        }
        return new Promise((_, r) => r(message(error)));
    }
}

module.exports = {
    promisify: promisify,
    rimraf: promisify(require('rimraf')),
    get_storage_remaining: () => get_folder_size(VIDEO_ROOT).then(bytes => max_video_dir_size - bytes),
    get_folder_size: get_folder_size,
    fs: {
        stat: promisify(fs.stat),
        writeFile: promisify(fs.writeFile),
        readFile: promisify(fs.readFile),
        unlink: promisify(fs.unlink),
        rename: promisify(fs.rename),
        mkdir_if_not_exists: function(path) {
            return promisify(fs.stat)(path).catch(function(error) {
                if (error.code === "ENOENT") {
                    return promisify(fs.mkdir)(path);
                }
                return new Promise((_, r) => r(error));
            });
        }
    },
    enumify: enumify,
    status: STATUS_CODES_ENUM,
    captialize: function(string) {
        if (!string) {
            return "";
        }
        return string[0].toUpperCase() + string.slice(1);
    },
    uuid: uuid,
    tiny_id: promisify(crypto.randomBytes)(3).then(buf => buf.toString('hex').toUpperCase()),
    ok: function(data) {
        // ¯\_(ツ)_/¯
        return not_ok_response(STATUS_CODES.ok, data);
    },
    not_ok: function(status, data) {
        return not_ok_response(STATUS_CODES[status], data);
    },
    handle_err: {
        sql: (logger, non_fatal) => create_error_handler(logger, non_fatal, error => `MYSQL ERROR: ${error.errno} (${error.code})`),
        mailgun: (logger, non_fatal) => create_error_handler(logger, non_fatal, error => `MAILGUN ERROR: ${error}`),
        status_res: handle_err_status_res,
        res: function(res, default_error_message) {
            // Invoke with (error) or ([error, data])
            // Since this is a catch handler and can only have a single argument
            return function(arg) {
                let error = arg;
                let data;
                if (Array.isArray(arg) && arg.length === 2) {
                    error = arg[0];
                    data = arg[1];
                }

                if (Object.prototype.hasOwnProperty.call(STATUS_CODES, error)) {
                    res.locals.log.general.error("Respond with status:", error);
                    res.send(not_ok_response(STATUS_CODES[error], data || {
                        message: default_error_message || "Internal Server Error",
                        color: "red"
                    }));
                } else if (typeof arg === "string") {
                    res.locals.log.general.error("Respond with status: error, message:", arg);
                    res.send(not_ok_response(STATUS_CODES.error, {
                        message: arg,
                        color: "red"
                    }));
                } else {
                    handle_err_status_res(res)(arg);
                }
            };
        }
    },
    mysql_options: mysql_options,
    query: query,
    /*
    Use for "UPDATE table SET param1=value1, param2=value2 WHERE param3=value3 AND param4=value4"
    Syntax: "UPDATE table" + utils.set_where({
        [param1]: value1,
        [param2]: value2
    }, {
        [param3]: value3,
        [param4]: value4
    })

    This will also escape all params and values
    */
    set_where: function(set, where) {
        return [
            ["SET", set, ", "],
            ["WHERE", where, " AND"]
        ].reduce(function(SQL_STR, [SQL_OPERATOR, params, DELIMITER]) {
            return SQL_STR + Object.entries(params).reduce(function(KEY_VALUE_STR, [param, val], i, arr) {
                return KEY_VALUE_STR + ` ${mysql.escapeId(param)}=${mysql.escape(val)}${i < arr.length - 1 ? DELIMITER : ""}`;
            }, " " + SQL_OPERATOR);
        }, "");
    },
    validate: {
        keys: function(obj, keys) {
            for (const key_data of keys) {
                let [key, validator, error_message] = Array.isArray(key_data) ? key_data : [key_data];
                validator = validator || (x => x);
                error_message = error_message || "Invalid Request";
                if (!Object.prototype.hasOwnProperty.call(obj, key) || !validator(obj[key])) {
                    log.general.error("key", key, "missing or invalid in obj", obj);
                    return new Promise((_, r) => r(error_message));
                }
            }
            return new Promise(r => r(obj));
        },
        email: function(str) {
            return typeof str === "string" && str.match(/.+@.+\..+/) !== null;
        },
        password: function(str) {
            return typeof str === "string" && zxcvbn(str.slice(0, Math.min(str.length, 100))).score > 0;
        },
        uuid: function(str) {
            return typeof str === "string" && str.match(/^([0-9]|[a-f]){8}-([0-9]|[a-f]){4}-4([0-9]|[a-f]){3}-[8,9,a,b]([0-9]|[a-f]){3}-([0-9]|[a-f]){12}$/i) !== null;
        }
    },
    resolve: data => new Promise(r => r(data)),
    reject: error => new Promise((_, r) => r(error)),
    setIntervalAndTriggerImmediate: function(callback, interval) {
        setTimeout(callback);
        return setInterval(callback, interval);
    }
};