UNPKG

pixl-server-storage

Version:

A key/value/list storage component for the pixl-server framework.

366 lines (304 loc) 9.52 kB
// Redis Storage Plugin // Copyright (c) 2015 - 2024 Joseph Huckaby // Released under the MIT License // Requires the 'ioredis' module from npm // npm install --save ioredis const Class = require("pixl-class"); const Component = require("pixl-server/component"); const Redis = require('ioredis'); const Tools = require("pixl-tools"); module.exports = Class.create({ __name: 'Redis', __parent: Component, defaultConfig: { host: 'localhost', port: 6379, commandTimeout: 5000, connectTimeout: 5000, username: "", password: "", keyPrefix: "", keyTemplate: "" }, startup: function(callback) { // setup Redis connection var self = this; this.logDebug(2, "Setting up Redis", this.config.get() ); this.setup(callback); }, setup: function(callback) { // setup Redis connection var self = this; var r_config = this.config.get(); this.keyPrefix = (r_config.keyPrefix || '').replace(/^\//, ''); if (this.keyPrefix && !this.keyPrefix.match(/\/$/)) this.keyPrefix += '/'; delete r_config.keyPrefix; this.keyTemplate = (r_config.keyTemplate || '').replace(/^\//, '').replace(/\/$/, ''); delete r_config.keyTemplate; if (!r_config.username.length) delete r_config.username; if (!r_config.password.length) delete r_config.password; r_config.lazyConnect = true; r_config.reconnectOnError = function(err) { return true; }; this.redis = new Redis(r_config); this.redis.on('error', function(err) { if (!self.storage.started) { return callback( new Error("Redis Startup Error: " + (err.message || err)) ); } // error after startup? Just log it I guess self.logError('redis', ''+err); }); // error this.redis.connect(function() { self.logDebug(8, "Successfully connected to Redis"); callback(); }); }, prepKey: function(key) { // prepare key for S3 based on config var md5 = Tools.digestHex(key, 'md5'); if (this.keyPrefix) { key = this.keyPrefix + key; } if (this.keyTemplate) { var idx = 0; var temp = this.keyTemplate.replace( /\#/g, function() { return md5.substr(idx++, 1); } ); key = Tools.substitute( temp, { key: key, md5: md5 } ); } return key; }, put: function(key, value, callback) { // store key+value in Redis var self = this; key = this.prepKey(key); if (this.storage.isBinaryKey(key)) { this.logDebug(9, "Storing Redis Binary Object: " + key, '' + value.length + ' bytes'); } else { this.logDebug(9, "Storing Redis JSON Object: " + key, this.debugLevel(10) ? value : null); value = JSON.stringify( value ); } this.redis.set( key, value, function(err) { if (err) { err.message = "Failed to store object: " + key + ": " + err; self.logError('redis', ''+err); } else self.logDebug(9, "Store complete: " + key); if (callback) callback(err); } ); }, putStream: function(key, inp, callback) { // store key+value in Redis using read stream var self = this; // The Redis API has no stream support. // So, we have to do this the RAM-hard way... var chunks = []; inp.on('data', function(chunk) { chunks.push( chunk ); } ); inp.on('end', function() { var buf = Buffer.concat(chunks); self.put( key, buf, callback ); } ); }, head: function(key, callback) { // head redis value given key var self = this; key = this.prepKey(key); // The Redis API has no way to head / ping an object. // So, we have to do this the RAM-hard way... this.redis.get( key, function(err, data) { if (err) { // an actual error err.message = "Failed to head key: " + key + ": " + err; self.logError('redis', ''+err); callback(err); } else if (!data) { // record not found // always use "NoSuchKey" in error code var err = new Error("Failed to head key: " + key + ": Not found"); err.code = "NoSuchKey"; callback( err, null ); } else { callback( null, { mod: 1, len: data.length } ); } } ); }, get: function(key, callback) { // fetch Redis value given key var self = this; key = this.prepKey(key); this.logDebug(9, "Fetching Redis Object: " + key); var func = this.storage.isBinaryKey(key) ? 'getBuffer' : 'get'; this.redis[func]( key, function(err, result) { if (!result) { if (err) { // an actual error err.message = "Failed to fetch key: " + key + ": " + err; self.logError('redis', ''+err); callback( err, null ); } else { // record not found // always use "NoSuchKey" in error code var err = new Error("Failed to fetch key: " + key + ": Not found"); err.code = "NoSuchKey"; callback( err, null ); } } else { if (self.storage.isBinaryKey(key)) { self.logDebug(9, "Binary fetch complete: " + key, '' + result.length + ' bytes'); } else { try { result = JSON.parse( result.toString() ); } catch (err) { self.logError('redis', "Failed to parse JSON record: " + key + ": " + err); callback( err, null ); return; } self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? result : null); } callback( null, result ); } } ); }, getBuffer: function(key, callback) { // fetch Redis buffer given key var self = this; key = this.prepKey(key); this.logDebug(9, "Fetching Redis Object: " + key); this.redis.getBuffer( key, function(err, result) { if (!result) { if (err) { // an actual error err.message = "Failed to fetch key: " + key + ": " + err; self.logError('redis', ''+err); callback( err, null ); } else { // record not found // always use "NoSuchKey" in error code var err = new Error("Failed to fetch key: " + key + ": Not found"); err.code = "NoSuchKey"; callback( err, null ); } } else { self.logDebug(9, "Binary fetch complete: " + key, '' + result.length + ' bytes'); callback( null, result ); } } ); }, getStream: function(key, callback) { // get readable stream to record value given key var self = this; // The Redis API has no stream support. // So, we have to do this the RAM-hard way... this.get( key, function(err, buf) { if (err) { // an actual error err.message = "Failed to fetch key: " + key + ": " + err; self.logError('redis', ''+err); return callback(err); } else if (!buf) { // record not found var err = new Error("Failed to fetch key: " + key + ": Not found"); err.code = "NoSuchKey"; return callback( err, null ); } var stream = new BufferStream(buf); callback(null, stream, { mod: 1, len: buf.length }); } ); }, getStreamRange: function(key, start, end, callback) { // get readable stream to record value given key and range var self = this; // The Redis API has no stream support. // So, we have to do this the RAM-hard way... this.get( key, function(err, buf) { if (err) { // an actual error err.message = "Failed to fetch key: " + key + ": " + err; self.logError('redis', ''+err); return callback(err); } else if (!buf) { // record not found var err = new Error("Failed to fetch key: " + key + ": Not found"); err.code = "NoSuchKey"; return callback( err, null ); } // validate byte range, now that we have the head info if (isNaN(start) && !isNaN(end)) { start = buf.length - end; end = buf.length ? buf.length - 1 : 0; } else if (!isNaN(start) && isNaN(end)) { end = buf.length ? buf.length - 1 : 0; } if (isNaN(start) || isNaN(end) || (start < 0) || (start >= buf.length) || (end < start) || (end >= buf.length)) { download.destroy(); callback( new Error("Invalid byte range (" + start + '-' + end + ") for key: " + key + " (len: " + buf.length + ")"), null ); return; } var range = buf.slice(start, end + 1); var stream = new BufferStream(range); callback(null, stream, { mod: 1, len: buf.length }); } ); }, delete: function(key, callback) { // delete Redis key given key var self = this; key = this.prepKey(key); this.logDebug(9, "Deleting Redis Object: " + key); this.redis.del( key, function(err, deleted) { if (!err && !deleted) { err = new Error("Failed to fetch key: " + key + ": Not found"); err.code = "NoSuchKey"; } if (err) { self.logError('redis', "Failed to delete object: " + key + ": " + err); } else self.logDebug(9, "Delete complete: " + key); callback(err); } ); }, runMaintenance: function(callback) { // run daily maintenance callback(); }, shutdown: function(callback) { // shutdown storage this.logDebug(2, "Shutting down Redis"); if (this.redis) { this.redis.quit(callback); this.redis = null; } else callback(); } }); // Modified the following snippet from node-streamifier: // Copyright (c) 2014 Gabriel Llamas, MIT Licensed var util = require('util'); var stream = require('stream'); var BufferStream = function (object, options) { if (object instanceof Buffer || typeof object === 'string') { options = options || {}; stream.Readable.call(this, { highWaterMark: options.highWaterMark, encoding: options.encoding }); } else { stream.Readable.call(this, { objectMode: true }); } this._object = object; }; util.inherits(BufferStream, stream.Readable); BufferStream.prototype._read = function () { this.push(this._object); this._object = null; };