'use strict'; exports = module.exports = { initialize: initialize, uninitialize: uninitialize, query: query, transaction: transaction, importFromFile: importFromFile, exportToFile: exportToFile, _clear: clear }; var assert = require('assert'), async = require('async'), BoxError = require('./boxerror.js'), child_process = require('child_process'), constants = require('./constants.js'), debug = require('debug')('box:database'), mysql = require('mysql'), once = require('once'), util = require('util'); var gConnectionPool = null, gDefaultConnection = null; const gDatabase = { hostname: '127.0.0.1', username: 'root', password: 'password', port: 3306, name: 'box' }; function initialize(callback) { assert.strictEqual(typeof callback, 'function'); if (gConnectionPool !== null) return callback(null); if (constants.TEST) { // see setupTest script how the mysql-server is run gDatabase.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim(); } gConnectionPool = mysql.createPool({ connectionLimit: 5, // this has to be > 1 since we store one connection as 'default'. the rest for transactions host: gDatabase.hostname, user: gDatabase.username, password: gDatabase.password, port: gDatabase.port, database: gDatabase.name, multipleStatements: false, ssl: false, timezone: 'Z' // mysql follows the SYSTEM timezone. on Cloudron, this is UTC }); gConnectionPool.on('connection', function (connection) { connection.query('USE ' + gDatabase.name); connection.query('SET SESSION sql_mode = \'strict_all_tables\''); }); reconnect(callback); } function uninitialize(callback) { if (gConnectionPool) { gConnectionPool.end(callback); gDefaultConnection = null; gConnectionPool = null; } else { callback(null); } } function reconnect(callback) { callback = callback ? once(callback) : function () {}; debug('reconnect: connecting to database'); gConnectionPool.getConnection(function (error, connection) { if (error) { debug('reconnect: unable to reestablish connection to database. Try again in 10 seconds.', error.message); return setTimeout(reconnect.bind(null, callback), 10000); } connection.on('error', function (error) { // by design, we catch all normal errors by providing callbacks. // this function should be invoked only when we have no callbacks pending and we have a fatal error assert(error.fatal, 'Non-fatal error on connection object'); debug('reconnect: unhandled mysql connection error. Will reconnect in 10 seconds', error); gDefaultConnection = null; // This is most likely an issue an can cause double callbacks from reconnect() setTimeout(reconnect.bind(null, callback), 10000); }); gDefaultConnection = connection; callback(null); }); } function clear(callback) { assert.strictEqual(typeof callback, 'function'); var cmd = util.format('mysql --host="%s" --user="%s" --password="%s" -Nse "SHOW TABLES" %s | grep -v "^migrations$" | while read table; do mysql --host="%s" --user="%s" --password="%s" -e "SET FOREIGN_KEY_CHECKS = 0; TRUNCATE TABLE $table" %s; done', gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name, gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name); child_process.exec(cmd, callback); } function beginTransaction(callback) { assert.strictEqual(typeof callback, 'function'); if (gConnectionPool === null) return callback(new BoxError(BoxError.DATABASE_ERROR, 'No database connection pool.')); gConnectionPool.getConnection(function (error, connection) { if (error) return callback(error); connection.beginTransaction(function (error) { if (error) return callback(error); return callback(null, connection); }); }); } function rollback(connection, commitError, callback) { assert.strictEqual(typeof callback, 'function'); connection.rollback(function (error) { if (error) debug('rollback: error when rolloing back', error); connection.release(); callback(commitError); }); } function commit(connection, callback) { assert.strictEqual(typeof callback, 'function'); connection.commit(function (error) { if (error) return rollback(connection, error, callback); connection.release(); return callback(null); }); } function query() { var args = Array.prototype.slice.call(arguments); var callback = args[args.length - 1]; assert.strictEqual(typeof callback, 'function'); if (gDefaultConnection === null) return callback(new BoxError(BoxError.DATABASE_ERROR, 'No connection to database')); gDefaultConnection.query.apply(gDefaultConnection, args); } function transaction(queries, callback) { assert(util.isArray(queries)); assert.strictEqual(typeof callback, 'function'); callback = once(callback); beginTransaction(function (error, connection) { if (error) return callback(error); connection.on('error', callback); async.mapSeries(queries, function iterator(query, done) { connection.query(query.query, query.args, done); }, function seriesDone(error, results) { if (error) return rollback(connection, callback.bind(null, error)); commit(connection, callback.bind(null, null, results)); }); }); } function importFromFile(file, callback) { assert.strictEqual(typeof file, 'string'); assert.strictEqual(typeof callback, 'function'); var cmd = `/usr/bin/mysql -h "${gDatabase.hostname}" -u ${gDatabase.username} -p${gDatabase.password} ${gDatabase.name} < ${file}`; async.series([ query.bind(null, 'CREATE DATABASE IF NOT EXISTS box'), child_process.exec.bind(null, cmd) ], callback); } function exportToFile(file, callback) { assert.strictEqual(typeof file, 'string'); assert.strictEqual(typeof callback, 'function'); // latest mysqldump enables column stats by default which is not present in MySQL 5.7 server // this option must not be set in production cloudrons which still use the old mysqldump const disableColStats = (constants.TEST && process.env.DESKTOP_SESSION !== 'ubuntu') ? '--column-statistics=0' : ''; var cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} ${disableColStats} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`; child_process.exec(cmd, callback); }