diff --git a/lib/dolly/replication.rb b/lib/dolly/replication.rb new file mode 100644 index 0000000..a3960ec --- /dev/null +++ b/lib/dolly/replication.rb @@ -0,0 +1,7 @@ +require 'dolly/replication/database' + +module Dolly + module Replication + include Dolly::Replication::Database + end +end diff --git a/lib/dolly/replication/database.rb b/lib/dolly/replication/database.rb new file mode 100644 index 0000000..9d3f327 --- /dev/null +++ b/lib/dolly/replication/database.rb @@ -0,0 +1,16 @@ +require "dolly/requests/base" +require "dolly/db_config" + +module Dolly + module Replication + module Database + include Dolly::DbConfig + + def database + options = {'name' => '_replicator'} + replicator_env = env.merge options + Requests::Base.new(replicator_env) + end + end + end +end diff --git a/lib/dolly/request.rb b/lib/dolly/request.rb index 93ea44a..857591a 100644 --- a/lib/dolly/request.rb +++ b/lib/dolly/request.rb @@ -1,119 +1,9 @@ -require "httparty" -require "dolly/bulk_document" +require "dolly/simple_replicator" module Dolly - - class Request - include HTTParty - REQUIRED_KEYS = %w/host port name/.freeze - - attr_accessor :database_name, :host, :port, :bulk_document - - def initialize options = {} - REQUIRED_KEYS.each do |key| - raise Dolly::MissingRequestConfigSettings.new(key) unless options[key] - end - - @host = options["host"] - @port = options["port"] - @database_name = options["name"] - @username = options["username"] - @password = options["password"] - @protocol = options["protocol"] - - @bulk_document = Dolly::BulkDocument.new [] - self.class.base_uri "#{protocol}://#{host}:#{port}" - end - - def get resource, data = nil - q = {query: values_to_json(data)} if data - request :get, full_path(resource), q - end - - def stats - request :get, "/#{database_name}" - end - - def put resource, data - request :put, full_path(resource), {body: data} - end - - def post resource, data - request :post, full_path(resource), {body: data} - end - - def delete resource - request :delete, full_path(resource), {} - end - - def attach resource, attachment_name, data, headers = {} - data = StringIO.new(data) if data.is_a?(String) - request :put, attachment_path(resource, attachment_name), {body: data, headers: headers} - end - - def protocol - @protocol ||= 'http' - end - - def uuids opts = {} - tools("_uuids", opts)["uuids"] - end - - def all_docs data = {} - data = values_to_json data.merge( include_docs: true ) - request :get, full_path('_all_docs'), {query: data} - end - - def request method, resource, data = nil - data ||= {} - data.merge!(basic_auth: auth_info) if auth_info.present? - headers = { 'Content-Type' => 'application/json' } - headers.merge! data[:headers] if data[:headers] - response = self.class.send method, resource, data.merge(headers: headers) - log_request(resource, response.code) if Dolly.log_requests? - if response.code == 404 - raise Dolly::ResourceNotFound - elsif (400..600).include? response.code - raise Dolly::ServerError.new( response ) - else - response - end - end - - private - def tools path, opts = nil - data = {} - q = "?#{CGI.unescape(opts.to_query)}" unless opts.blank? - data.merge!(basic_auth: auth_info) if auth_info.present? - JSON::parse self.class.get("/#{path}#{q}", data) - end - - def auth_info - return nil unless @username.present? - {username: @username, password: @password} - end - - def values_to_json hash - hash.reduce({}){|h, v| h[v.first] = v.last.to_json; h} - end - - def full_path resource - "/#{database_name}/#{resource}" - end - - def attachment_path resource, attachment_name - "#{full_path(resource)}/#{attachment_name}" - end - - def log_request resource, response_code - log_value = ->(resource, response_code) { "Query: #{resource}, Response Code: #{response_code}" } - case response_code - when 200..399 - Dolly.logger.info log_value[resource, response_code] - when 400..600 - Dolly.logger.warn log_value[resource, response_code] - end + class Request < Dolly::Requests::Base + def replicate! target_db, opts={} + Dolly::SimpleReplicator.new(self, target_db, opts).replicate! end end - end diff --git a/lib/dolly/requests/base.rb b/lib/dolly/requests/base.rb new file mode 100644 index 0000000..9a2cf60 --- /dev/null +++ b/lib/dolly/requests/base.rb @@ -0,0 +1,121 @@ +require "httparty" +require "dolly/bulk_document" + +module Dolly + + module Requests + class Base + include HTTParty + REQUIRED_KEYS = %w/host port name/.freeze + + attr_accessor :database_name, :host, :port, :bulk_document + + def initialize options = {} + REQUIRED_KEYS.each do |key| + raise Dolly::MissingRequestConfigSettings.new(key) unless options[key] + end + + @host = options["host"] + @port = options["port"] + @database_name = options["name"] + @username = options["username"] + @password = options["password"] + @protocol = options["protocol"] + + @bulk_document = Dolly::BulkDocument.new [] + self.class.base_uri "#{protocol}://#{host}:#{port}" + end + + def get resource, data = nil + q = {query: values_to_json(data)} if data + request :get, full_path(resource), q + end + + def stats + request :get, "/#{database_name}" + end + + def put resource, data + request :put, full_path(resource), {body: data} + end + + def post resource, data + request :post, full_path(resource), {body: data} + end + + def delete resource + request :delete, full_path(resource), {} + end + + def attach resource, attachment_name, data, headers = {} + data = StringIO.new(data) if data.is_a?(String) + request :put, attachment_path(resource, attachment_name), {body: data, headers: headers} + end + + def protocol + @protocol ||= 'http' + end + + def uuids opts = {} + tools("_uuids", opts)["uuids"] + end + + def all_docs data = {} + data = values_to_json data.merge( include_docs: true ) + request :get, full_path('_all_docs'), {query: data} + end + + def request method, resource, data = nil + data ||= {} + data.merge!(basic_auth: auth_info) if auth_info.present? + headers = { 'Content-Type' => 'application/json' } + headers.merge! data[:headers] if data[:headers] + response = self.class.send method, resource, data.merge(headers: headers) + log_request(resource, response.code) if Dolly.log_requests? + if response.code == 404 + raise Dolly::ResourceNotFound + elsif (400..600).include? response.code + raise Dolly::ServerError.new( response ) + else + response + end + end + + private + def tools path, opts = nil + data = {} + q = "?#{CGI.unescape(opts.to_query)}" unless opts.blank? + data.merge!(basic_auth: auth_info) if auth_info.present? + JSON::parse self.class.get("/#{path}#{q}", data) + end + + def auth_info + return nil unless @username.present? + {username: @username, password: @password} + end + + def values_to_json hash + hash.reduce({}){|h, v| h[v.first] = v.last.to_json; h} + end + + def full_path resource + "/#{database_name}/#{resource}" + end + + def attachment_path resource, attachment_name + "#{full_path(resource)}/#{attachment_name}" + end + + def log_request resource, response_code + log_value = ->(resource, response_code) { "Query: #{resource}, Response Code: #{response_code}" } + case response_code + when 200..399 + Dolly.logger.info log_value[resource, response_code] + when 400..600 + Dolly.logger.warn log_value[resource, response_code] + end + end + end + end + +end diff --git a/lib/dolly/simple_replicator.rb b/lib/dolly/simple_replicator.rb new file mode 100644 index 0000000..2c06509 --- /dev/null +++ b/lib/dolly/simple_replicator.rb @@ -0,0 +1,29 @@ +require 'dolly/replication/database' + +module Dolly + class SimpleReplicator + include Dolly::Replication::Database + + attr_reader :source_db, :target_db + + def initialize source_db, target_db, opts={} + #@opts = opts #TODO add options for difference replications + @source_db = source_db + @target_db = target_db + end + + def replicate! + database.request :post, '/_replicate', body: request_body + end + + private + + def request_body + { + source: source_db.database_name, + target: target_db.database_name + }.to_json + end + + end +end diff --git a/test/request_test.rb b/test/request_test.rb index beddc4c..9a72827 100644 --- a/test/request_test.rb +++ b/test/request_test.rb @@ -22,4 +22,34 @@ def setup Dolly::Request.new @default_settings.dup.delete('port') end end + + class ReplicateTest < RequestTest + test 'posts a replication request to the replicator database' do + db = Dolly::Request.new @default_settings + other_db = Dolly::Request.new @default_settings.merge({'name' => 'database_2'}) + resp = { + "ok" => true, + "session_id" => "8fa6f7bfdad58a9aa229d6c482a06012", + "source_last_seq" => 5, + "replication_id_version" => 3, + "history" => [ + { + "session_id" => "8fa6f7bfdad58a9aa229d6c482a06012", + "start_time" => Date.today.to_s, + "end_time" => Date.today.to_s, + "start_last_seq" => 0, + "end_last_seq" => 5, + "recorded_seq" => 5, + "missing_checked" => 4, + "missing_found" => 0, + "docs_read" => 0, + "docs_written" => 0, + "doc_write_failures" => 0 + } + ] + } + FakeWeb.register_uri :post, "http://localhost:5984/_replicate", body: resp.to_json + assert db.replicate!(other_db) + end + end end