couchrest_changes 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Rakefile +93 -0
- data/Readme.md +60 -0
- data/lib/couchrest/changes/config.rb +114 -0
- data/lib/couchrest/changes/version.rb +5 -0
- data/lib/couchrest/changes.rb +116 -0
- data/test/test_helper.rb +8 -0
- metadata +166 -0
data/Rakefile
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "highline/import"
|
3
|
+
require "pty"
|
4
|
+
require "fileutils"
|
5
|
+
require 'rake/testtask'
|
6
|
+
|
7
|
+
##
|
8
|
+
## HELPER
|
9
|
+
##
|
10
|
+
|
11
|
+
def run(cmd)
|
12
|
+
PTY.spawn(cmd) do |output, input, pid|
|
13
|
+
begin
|
14
|
+
while line = output.gets do
|
15
|
+
puts line
|
16
|
+
end
|
17
|
+
rescue Errno::EIO
|
18
|
+
end
|
19
|
+
end
|
20
|
+
rescue PTY::ChildExited
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
## GEM BUILDING AND INSTALLING
|
25
|
+
##
|
26
|
+
|
27
|
+
$spec_path = 'couchrest_changes.gemspec'
|
28
|
+
$spec = eval(File.read($spec_path))
|
29
|
+
$base_dir = File.dirname(__FILE__)
|
30
|
+
$gem_path = File.join($base_dir, 'pkg', "#{$spec.name}-#{$spec.version}.gem")
|
31
|
+
|
32
|
+
def built_gem_path
|
33
|
+
Dir[File.join($base_dir, "#{$spec.name}-*.gem")].sort_by{|f| File.mtime(f)}.last
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "Build #{$spec.name}-#{$spec.version}.gem into the pkg directory"
|
37
|
+
task 'build' do
|
38
|
+
FileUtils.mkdir_p(File.join($base_dir, 'pkg'))
|
39
|
+
FileUtils.rm($gem_path) if File.exists?($gem_path)
|
40
|
+
run "gem build -V '#{$spec_path}'"
|
41
|
+
file_name = File.basename(built_gem_path)
|
42
|
+
FileUtils.mv(built_gem_path, 'pkg')
|
43
|
+
say "#{$spec.name} #{$spec.version} built to pkg/#{file_name}"
|
44
|
+
end
|
45
|
+
|
46
|
+
desc "Install #{$spec.name}-#{$spec.version}.gem into either system-wide or user gems"
|
47
|
+
task 'install' do
|
48
|
+
if !File.exists?($gem_path)
|
49
|
+
say("Could not file #{$gem_path}. Try running 'rake build'")
|
50
|
+
else
|
51
|
+
if ENV["USER"] == "root"
|
52
|
+
run "gem install '#{$gem_path}'"
|
53
|
+
else
|
54
|
+
home_gem_path = Gem.path.grep(/home/).first
|
55
|
+
say("You are installing as an unprivileged user, which will result in the installation being placed in '#{home_gem_path}'.")
|
56
|
+
if agree("Do you want to continue installing to #{home_gem_path}? ")
|
57
|
+
run "gem install '#{$gem_path}' --user-install"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
desc "Uninstall #{$spec.name}-#{$spec.version}.gem from either system-wide or user gems"
|
64
|
+
task 'uninstall' do
|
65
|
+
if ENV["USER"] == "root"
|
66
|
+
say("Removing #{$spec.name}-#{$spec.version}.gem from system-wide gems")
|
67
|
+
run "gem uninstall '#{$spec.name}' --version #{$spec.version} --verbose -x -I"
|
68
|
+
else
|
69
|
+
say("Removing #{$spec.name}-#{$spec.version}.gem from user's gems")
|
70
|
+
run "gem uninstall '#{$spec.name}' --version #{$spec.version} --verbose --user-install -x -I"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
## TESTING
|
76
|
+
##
|
77
|
+
|
78
|
+
Rake::TestTask.new do |t|
|
79
|
+
t.pattern = "test/unit/*_test.rb"
|
80
|
+
end
|
81
|
+
task :default => :test
|
82
|
+
|
83
|
+
##
|
84
|
+
## DOCUMENTATION
|
85
|
+
##
|
86
|
+
|
87
|
+
# require 'rdoc/task'
|
88
|
+
|
89
|
+
# Rake::RDocTask.new do |rd|
|
90
|
+
# rd.main = "README.rdoc"
|
91
|
+
# rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*")
|
92
|
+
# rd.title = 'Your application title'
|
93
|
+
# end
|
data/Readme.md
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
CouchRest::Changes - keeping track of changes to your couch
|
2
|
+
------------------------------------------------------------
|
3
|
+
|
4
|
+
``CouchRest::Changes`` let's you observe a couch database for changes and react upon them.
|
5
|
+
|
6
|
+
Following the changes of a couch is as easy as
|
7
|
+
```ruby
|
8
|
+
users = CouchRest::Changes.new('users')
|
9
|
+
```
|
10
|
+
|
11
|
+
Callbacks can be defined in blocks:
|
12
|
+
```ruby
|
13
|
+
users.created do |hash|
|
14
|
+
puts "A new user was created with the id: #{hash[:id]}
|
15
|
+
end
|
16
|
+
```
|
17
|
+
|
18
|
+
To start listening just call
|
19
|
+
```ruby
|
20
|
+
users.listen
|
21
|
+
```
|
22
|
+
|
23
|
+
This program is written in Ruby and is distributed under the following license:
|
24
|
+
|
25
|
+
> GNU Affero General Public License
|
26
|
+
> Version 3.0 or higher
|
27
|
+
> http://www.gnu.org/licenses/agpl-3.0.html
|
28
|
+
|
29
|
+
Installation
|
30
|
+
---------------------
|
31
|
+
|
32
|
+
Just add couchrest_changes to your gemfile.
|
33
|
+
|
34
|
+
Configuration
|
35
|
+
---------------------
|
36
|
+
|
37
|
+
``couchrest_changes`` can be configured through ``CouchRest::Changes::Config``
|
38
|
+
|
39
|
+
The default options are similar to the ones used by CouchRest::Model:
|
40
|
+
|
41
|
+
|
42
|
+
```yaml
|
43
|
+
# couch connection configuration
|
44
|
+
connection:
|
45
|
+
protocol: "http"
|
46
|
+
host: "localhost"
|
47
|
+
port: 5984
|
48
|
+
username: ~
|
49
|
+
password: ~
|
50
|
+
prefix: ""
|
51
|
+
suffix: ""
|
52
|
+
|
53
|
+
# file to store the last processed user record in so we can resume after
|
54
|
+
# a restart:
|
55
|
+
seq_file: "/var/log/couch_changes_users.seq"
|
56
|
+
|
57
|
+
# Configure log_file like this if you want to log to a file instead of syslog:
|
58
|
+
# log_file: "/var/log/couch_changes.log"
|
59
|
+
log_level: debug
|
60
|
+
```
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module CouchRest
|
4
|
+
class Changes
|
5
|
+
module Config
|
6
|
+
extend self
|
7
|
+
|
8
|
+
attr_accessor :connection
|
9
|
+
attr_accessor :seq_file
|
10
|
+
attr_accessor :log_file
|
11
|
+
attr_writer :log_level
|
12
|
+
attr_accessor :logger
|
13
|
+
attr_accessor :options
|
14
|
+
|
15
|
+
def load(base_dir, *configs)
|
16
|
+
@base_dir = Pathname.new(base_dir)
|
17
|
+
loaded = configs.collect do |file_path|
|
18
|
+
file = find_file(file_path)
|
19
|
+
load_config(file)
|
20
|
+
end
|
21
|
+
init_logger
|
22
|
+
log_loaded_configs(loaded.compact)
|
23
|
+
logger.info "Observing #{couch_host_without_password}"
|
24
|
+
return self
|
25
|
+
end
|
26
|
+
|
27
|
+
def couch_host(conf = nil)
|
28
|
+
conf ||= connection
|
29
|
+
userinfo = [conf[:username], conf[:password]].compact.join(':')
|
30
|
+
userinfo += '@' unless userinfo.empty?
|
31
|
+
"#{conf[:protocol]}://#{userinfo}#{conf[:host]}:#{conf[:port]}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def couch_host_without_password
|
35
|
+
couch_host connection.merge({:password => nil})
|
36
|
+
end
|
37
|
+
|
38
|
+
def complete_db_name(db_name)
|
39
|
+
[connection[:prefix], db_name, connection[:suffix]].
|
40
|
+
compact.
|
41
|
+
reject{|part| part == ""}.
|
42
|
+
join('_')
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def init_logger
|
48
|
+
if log_file
|
49
|
+
require 'logger'
|
50
|
+
@logger = Logger.new(log_file)
|
51
|
+
else
|
52
|
+
require 'syslog/logger'
|
53
|
+
@logger = Syslog::Logger.new('leap_key_daemon')
|
54
|
+
end
|
55
|
+
@logger.level = Logger.const_get(log_level.upcase)
|
56
|
+
end
|
57
|
+
|
58
|
+
def load_config(file_path)
|
59
|
+
return unless file_path
|
60
|
+
load_settings YAML.load(File.read(file_path)), file_path
|
61
|
+
return file_path
|
62
|
+
end
|
63
|
+
|
64
|
+
def load_settings(hash, file_path)
|
65
|
+
return unless hash
|
66
|
+
hash.each do |key, value|
|
67
|
+
begin
|
68
|
+
apply_setting(key, value)
|
69
|
+
rescue NoMethodError => exc
|
70
|
+
# log might not have been configured yet correctly
|
71
|
+
# so better also print this
|
72
|
+
STDERR.puts "Error in file #{file_path}"
|
73
|
+
STDERR.puts "'#{key}' is not a valid option"
|
74
|
+
init_logger
|
75
|
+
logger.warn "Error in file #{file_path}"
|
76
|
+
logger.warn "'#{key}' is not a valid option"
|
77
|
+
logger.debug exc
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def apply_setting(key, value)
|
83
|
+
if value.is_a? Hash
|
84
|
+
value = symbolize_keys(value)
|
85
|
+
end
|
86
|
+
self.send("#{key}=", value)
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.symbolize_keys(hsh)
|
90
|
+
newhsh = {}
|
91
|
+
hsh.keys.each do |key|
|
92
|
+
newhsh[key.to_sym] = hsh[key]
|
93
|
+
end
|
94
|
+
newhsh
|
95
|
+
end
|
96
|
+
|
97
|
+
def find_file(file_path)
|
98
|
+
return unless file_path
|
99
|
+
filenames = [Pathname.new(file_path), @base_dir + file_path]
|
100
|
+
filenames.find{|f| f.file?}
|
101
|
+
end
|
102
|
+
|
103
|
+
def log_loaded_configs(files)
|
104
|
+
files.each do |file|
|
105
|
+
logger.info "Loaded config from #{file} ."
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def log_level
|
110
|
+
@log_level || 'info'
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'couchrest'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
require 'couchrest/changes/config'
|
6
|
+
|
7
|
+
module CouchRest
|
8
|
+
class Changes
|
9
|
+
|
10
|
+
attr_writer :logger
|
11
|
+
|
12
|
+
def initialize(db_name)
|
13
|
+
db_name = Config.complete_db_name(db_name)
|
14
|
+
logger.info "Tracking #{db_name}"
|
15
|
+
@db = CouchRest.new(Config.couch_host).database(db_name)
|
16
|
+
@seq_filename = Config.seq_file
|
17
|
+
read_seq(@seq_filename)
|
18
|
+
end
|
19
|
+
|
20
|
+
# triggered when a document was newly created
|
21
|
+
def created(hash = {}, &block)
|
22
|
+
run_or_define_hook :created, hash, &block
|
23
|
+
end
|
24
|
+
|
25
|
+
# triggered when a document was deleted
|
26
|
+
def deleted(hash = {}, &block)
|
27
|
+
run_or_define_hook :deleted, hash, &block
|
28
|
+
end
|
29
|
+
|
30
|
+
# triggered when an existing document was updated
|
31
|
+
def updated(hash = {}, &block)
|
32
|
+
run_or_define_hook :updated, hash, &block
|
33
|
+
end
|
34
|
+
|
35
|
+
# triggered whenever a document was changed
|
36
|
+
def changed(hash = {}, &block)
|
37
|
+
run_or_define_hook :changed, hash, &block
|
38
|
+
end
|
39
|
+
|
40
|
+
def listen
|
41
|
+
logger.info "listening..."
|
42
|
+
logger.debug "Starting at sequence #{since}"
|
43
|
+
result = db.changes :feed => :continuous, :since => since, :heartbeat => 1000 do |hash|
|
44
|
+
callbacks(hash)
|
45
|
+
store_seq(hash["seq"])
|
46
|
+
end
|
47
|
+
logger.info "couch stream ended unexpectedly."
|
48
|
+
logger.debug result.inspect
|
49
|
+
end
|
50
|
+
|
51
|
+
protected
|
52
|
+
|
53
|
+
def logger
|
54
|
+
logger ||= Config.logger
|
55
|
+
end
|
56
|
+
|
57
|
+
def db
|
58
|
+
@db
|
59
|
+
end
|
60
|
+
|
61
|
+
def since
|
62
|
+
@since ||= 0 # fetch_last_seq
|
63
|
+
end
|
64
|
+
|
65
|
+
def callbacks(hash)
|
66
|
+
# let's not track design document changes
|
67
|
+
return if hash['id'].start_with? '_design/'
|
68
|
+
return unless changes = hash["changes"]
|
69
|
+
changed(hash)
|
70
|
+
return deleted(hash) if hash["deleted"]
|
71
|
+
return created(hash) if changes[0]["rev"].start_with?('1-')
|
72
|
+
updated(hash)
|
73
|
+
end
|
74
|
+
|
75
|
+
def run_or_define_hook(event, hash = {}, &block)
|
76
|
+
@callbacks ||= {}
|
77
|
+
if block_given?
|
78
|
+
@callbacks[event] = block
|
79
|
+
else
|
80
|
+
@callbacks[event] && @callbacks[event].call(hash)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def read_seq(seq_filename)
|
85
|
+
logger.debug "Looking up sequence here: #{seq_filename}"
|
86
|
+
FileUtils.touch(seq_filename)
|
87
|
+
unless File.writable?(seq_filename)
|
88
|
+
raise StandardError.new("Can't access sequence file")
|
89
|
+
end
|
90
|
+
@since = File.read(seq_filename)
|
91
|
+
if @since == ''
|
92
|
+
@since = nil
|
93
|
+
logger.debug "Found no sequence in the file."
|
94
|
+
else
|
95
|
+
logger.debug "Found sequence: #{@since}"
|
96
|
+
end
|
97
|
+
rescue Errno::ENOENT => e
|
98
|
+
logger.warn "No sequence file found. Starting from scratch"
|
99
|
+
end
|
100
|
+
|
101
|
+
def store_seq(seq)
|
102
|
+
File.write @seq_filename, MultiJson.dump(seq)
|
103
|
+
end
|
104
|
+
|
105
|
+
#
|
106
|
+
# UNUSED: this is useful for only following new sequences.
|
107
|
+
# might also require .to_json to work on bigcouch.
|
108
|
+
#
|
109
|
+
def fetch_last_seq
|
110
|
+
hash = db.changes :limit => 1, :descending => true
|
111
|
+
logger.info "starting at seq: " + hash["last_seq"]
|
112
|
+
return hash["last_seq"]
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: couchrest_changes
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Azul
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-12-20 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: couchrest
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.1.3
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.1.3
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: yajl-ruby
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: syslog_logger
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 2.0.0
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.0.0
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: minitest
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 3.2.0
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 3.2.0
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: mocha
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: rake
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: highline
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
description: Watches the couch database for changes and triggers callbacks defined
|
127
|
+
for creation, deletes and updates.
|
128
|
+
email:
|
129
|
+
- azul@leap.se
|
130
|
+
executables: []
|
131
|
+
extensions: []
|
132
|
+
extra_rdoc_files: []
|
133
|
+
files:
|
134
|
+
- lib/couchrest/changes/config.rb
|
135
|
+
- lib/couchrest/changes/version.rb
|
136
|
+
- lib/couchrest/changes.rb
|
137
|
+
- Rakefile
|
138
|
+
- Readme.md
|
139
|
+
- test/test_helper.rb
|
140
|
+
homepage: https://leap.se
|
141
|
+
licenses: []
|
142
|
+
post_install_message:
|
143
|
+
rdoc_options: []
|
144
|
+
require_paths:
|
145
|
+
- lib
|
146
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
147
|
+
none: false
|
148
|
+
requirements:
|
149
|
+
- - ! '>='
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ! '>='
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
requirements: []
|
159
|
+
rubyforge_project:
|
160
|
+
rubygems_version: 1.8.25
|
161
|
+
signing_key:
|
162
|
+
specification_version: 3
|
163
|
+
summary: CouchRest::Changes - Observe a couch database for changes and react upon
|
164
|
+
them
|
165
|
+
test_files:
|
166
|
+
- test/test_helper.rb
|