diff --git a/README.md b/README.md index 1830f40..b417ffb 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ * Search keys * Can be mounted to Rails applications as engine * Can connect to multiple databases +* Export keys as CSV ## Installation diff --git a/config.yml b/config.yml index bfc2b12..c34f380 100644 --- a/config.yml +++ b/config.yml @@ -1,6 +1,7 @@ connections: localhost: url: redis://127.0.0.1:6379/0 + exclude_pattern: "^.+/total/.+$" other: host: 127.0.0.1 port: 6379 diff --git a/lib/redis-browser.rb b/lib/redis-browser.rb index cae8ec7..abe0f24 100644 --- a/lib/redis-browser.rb +++ b/lib/redis-browser.rb @@ -1,3 +1,4 @@ +require 'csv' require 'sinatra/base' require 'multi_json' require 'sinatra/json' @@ -11,7 +12,10 @@ module RedisBrowser DEFAULTS = { 'connections' => { - 'default' => 'redis://127.0.0.1:6379/0' + 'default' => { + 'url' => 'redis://127.0.0.1:6379/0', + 'exclude-pattern' => '' + } } } diff --git a/lib/redis-browser/browser.rb b/lib/redis-browser/browser.rb index c3c5093..38b7af0 100644 --- a/lib/redis-browser/browser.rb +++ b/lib/redis-browser/browser.rb @@ -125,6 +125,27 @@ def get(key, opts = {}) }.merge(data) end + def exportCSV(include_pattern, exclude_pattern) + key = include_pattern + key << "*" unless key.end_with?("*") + exclude_rx = Regexp.new(exclude_pattern) + keys = redis.keys(key).select { |k| exclude_rx.match(k).nil? } + values = redis.multi do |multi| + keys.each { |k| multi.get(k) } + end + kv = keys.zip(values) + + csv_string = CSV.generate do |csv| + kv.each do |k, v| + line = k.split(/[:\/]/) << v + csv << line + end + end + csv_string + rescue => ex + {:error => ex.message} + end + def ping redis.ping == "PONG" {:ok => 1} diff --git a/lib/redis-browser/public/css/app.css b/lib/redis-browser/public/css/app.css index 2fcf050..3802d38 100644 --- a/lib/redis-browser/public/css/app.css +++ b/lib/redis-browser/public/css/app.css @@ -34,11 +34,16 @@ body { margin-right: 5px; } +.help-block { + font-size: 0.9em; + color: gray; +} + .value-list-index { width: 30px; } .value-zset-score { width: 30px; } .per-page { margin-top: 25px; } -.connection-info { padding-right: 20px; } +.header-item { padding-right: 20px; } .keys-search { width: 100px; } #http-loader { diff --git a/lib/redis-browser/templates/coffee/app.coffee b/lib/redis-browser/templates/coffee/app.coffee index 0ad1561..7960516 100644 --- a/lib/redis-browser/templates/coffee/app.coffee +++ b/lib/redis-browser/templates/coffee/app.coffee @@ -22,6 +22,10 @@ app.factory 'API', ['$http', ($http) -> (connection) -> ps = {connection: connection} { + exportCSV: (params) -> $http.get("#{jsEnv.root_path}export-csv", { + params: angular.extend({}, ps, params) + }).then (e) -> e, + ping: () -> $http.get("#{jsEnv.root_path}ping.json", { params: ps }).then (e) -> e.data, @@ -97,6 +101,45 @@ app.factory 'API', ['$http', ($http) -> backdropFade: true dialogFade: true + $scope.export = + filename: "redis-dump.csv" + + open: -> + $scope.export.show = true + $scope.export.include = $scope.key.full || "*" + $scope.export.exclude = $scope.connections[$scope.config.connection].exclude_pattern + + close: -> + $scope.export.show = false + $scope.export.error = null + + run: -> + $scope.api.exportCSV( + include: $scope.export.include, + exclude: $scope.export.exclude + ).then (response) -> + $scope.export.error = null + + content = response.data + if response.headers('Content-Type').includes("text/csv") + hiddenElement = document.createElement('a') + + hiddenElement.href = 'data:attachment/csv,' + encodeURI(content) + hiddenElement.target = '_blank' + hiddenElement.download = $scope.export.filename + hiddenElement.click() + + $scope.config.close() + + destroyA = setInterval( -> + hiddenElement.remove() + clearInterval(destroyA) + , 5000 + ) + + else + $scope.export.error = content.error + $scope.api = API($scope.config.connection) diff --git a/lib/redis-browser/templates/index.slim b/lib/redis-browser/templates/index.slim index 7d21d86..2d8c44a 100644 --- a/lib/redis-browser/templates/index.slim +++ b/lib/redis-browser/templates/index.slim @@ -48,6 +48,43 @@ html ng-app="browser" ' {{ config.error }} button.btn.btn-success type="submit" ng-click="config.save()" Save + div modal="export.show" close="export.close()" options="config.modalOpts" + .form-horizontal + .modal-header + button.close type="button" ng-click="export.close()" × + h4 Export CSV + .modal-body + .control-group + label.control-label for="export-include" Include keys + .controls + input.form-control id="export-include" ng-model="export.include" ng-required="required" type="text" + + .control-group + label.control-label for="export-exclude" Exclude keys pattern + .controls + input.form-control id="export-exclude" ng-model="export.exclude" type="text" + + .control-group + label.control-label for="export-filename" File name + .controls + input.form-control id="export-filename" ng-model="export.filename" ng-required="required" type="text" + + span.help-block + b Include keys + | is keys to include in the output as per + a href="http://redis.io/commands/KEYS" target="_blank" KEYS + | . + br + b Exclude keys pattern + | is a + a href="https://en.wikipedia.org/wiki/Regular_expression" target="_blank" regex + | which excludes the keys from the output. + + .modal-footer + span.alert.alert-error.pull-left ng-show="export.error" + ' {{ export.error }} + button.btn.btn-success type="submit" ng-click="export.run()" Export + .navbar.navbar-inverse.navbar-fixed-top .navbar-inner .container-fluid @@ -58,8 +95,10 @@ html ng-app="browser" .nav-collapse.collapse p.pull-right + button.btn.btn-default ng-click="export.open()" Export CSV + p.pull-right.header-item button.btn.btn-success ng-click="config.open()" Configure - p.navbar-text.pull-right.connection-info + p.navbar-text.pull-right.header-item ' Connected to strong ' {{ config.connection }} diff --git a/lib/redis-browser/web.rb b/lib/redis-browser/web.rb index 0ca5541..b8d5217 100644 --- a/lib/redis-browser/web.rb +++ b/lib/redis-browser/web.rb @@ -34,6 +34,17 @@ class Web < Sinatra::Base slim :index end + get '/export-csv' do + result = browser.exportCSV(params[:include], params[:exclude]) + if result.is_a? String + content_type 'text/csv' + attachment "redis-dump.csv" + result + else + json result + end + end + get '/ping.json' do json browser.ping end