ryespy 0.7.0 → 1.0.0
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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/.travis.yml +15 -1
- data/CHANGELOG.md +24 -0
- data/README.md +155 -34
- data/bin/ryespy +186 -80
- data/lib/ryespy.rb +4 -49
- data/lib/ryespy/app.rb +159 -0
- data/lib/ryespy/listener/amzn_s3.rb +37 -0
- data/lib/ryespy/listener/base.rb +34 -0
- data/lib/ryespy/listener/fogable.rb +59 -0
- data/lib/ryespy/listener/ftp.rb +92 -0
- data/lib/ryespy/listener/goog_cs.rb +37 -0
- data/lib/ryespy/listener/imap.rb +79 -0
- data/lib/ryespy/listener/rax_cf.rb +53 -0
- data/lib/ryespy/notifier/sidekiq.rb +69 -0
- data/lib/ryespy/version.rb +1 -1
- data/ryespy.gemspec +8 -7
- data/test/helper.rb +41 -0
- data/test/ryespy/app_test.rb +348 -0
- data/test/ryespy/listener/amzn_s3_test.rb +138 -0
- data/test/ryespy/listener/ftp_test.rb +96 -0
- data/test/ryespy/listener/goog_cs_test.rb +138 -0
- data/test/ryespy/listener/imap_test.rb +68 -0
- data/test/ryespy/listener/rax_cf_test.rb +142 -0
- data/test/ryespy/notifier/sidekiq_test.rb +44 -0
- data/test/ryespy/version_test.rb +1 -1
- metadata +109 -32
- data/lib/ryespy/config.rb +0 -83
- data/lib/ryespy/listeners/ftp.rb +0 -86
- data/lib/ryespy/listeners/imap.rb +0 -74
- data/lib/ryespy/notifiers/sidekiq.rb +0 -49
- data/lib/ryespy/redis_conn.rb +0 -32
- data/test/ryespy/config_test.rb +0 -223
- data/test/ryespy_test.rb +0 -0
data/lib/ryespy.rb
CHANGED
@@ -1,53 +1,8 @@
|
|
1
|
-
require 'logger'
|
2
|
-
|
3
1
|
require_relative 'ryespy/version'
|
4
|
-
require_relative 'ryespy/config'
|
5
|
-
require_relative 'ryespy/redis_conn'
|
6
|
-
|
7
|
-
require_relative 'ryespy/listeners/imap'
|
8
|
-
require_relative 'ryespy/listeners/ftp'
|
9
2
|
|
10
|
-
require_relative 'ryespy/
|
3
|
+
require_relative 'ryespy/app'
|
11
4
|
|
5
|
+
require_relative 'ryespy/listener/base'
|
6
|
+
# ryespy/listener/X dynamically required in ryespy/app.rb
|
12
7
|
|
13
|
-
|
14
|
-
|
15
|
-
extend self
|
16
|
-
|
17
|
-
def config
|
18
|
-
@config ||= Ryespy::Config.new
|
19
|
-
end
|
20
|
-
|
21
|
-
def configure
|
22
|
-
yield config
|
23
|
-
|
24
|
-
Ryespy.logger.debug { "Configured #{Ryespy.config.to_s}" }
|
25
|
-
end
|
26
|
-
|
27
|
-
def logger
|
28
|
-
unless @logger
|
29
|
-
@logger = Logger.new($stdout)
|
30
|
-
|
31
|
-
@logger.level = Logger.const_get(Ryespy.config.log_level)
|
32
|
-
end
|
33
|
-
|
34
|
-
@logger
|
35
|
-
end
|
36
|
-
|
37
|
-
def redis
|
38
|
-
@redis ||= Ryespy::RedisConn.new(Ryespy.config.redis_url).redis
|
39
|
-
end
|
40
|
-
|
41
|
-
def notifiers
|
42
|
-
unless @notifiers
|
43
|
-
@notifiers = []
|
44
|
-
|
45
|
-
Ryespy.config.notifiers[:sidekiq].each do |notifier_instance|
|
46
|
-
@notifiers << Ryespy::Notifier::Sidekiq.new(notifier_instance)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
@notifiers
|
51
|
-
end
|
52
|
-
|
53
|
-
end
|
8
|
+
require_relative 'ryespy/notifier/sidekiq'
|
data/lib/ryespy/app.rb
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'redis'
|
4
|
+
require 'redis-namespace'
|
5
|
+
|
6
|
+
# listener dynamically required in App#setup
|
7
|
+
|
8
|
+
|
9
|
+
module Ryespy
|
10
|
+
class App
|
11
|
+
|
12
|
+
def self.config_defaults
|
13
|
+
{
|
14
|
+
:log_level => :INFO,
|
15
|
+
:polling_interval => 60,
|
16
|
+
:redis_ns_ryespy => 'ryespy',
|
17
|
+
:redis_ns_notifiers => 'resque',
|
18
|
+
:imap => {
|
19
|
+
:port => 993,
|
20
|
+
:ssl => true,
|
21
|
+
:filters => ['INBOX'], # mailboxes
|
22
|
+
},
|
23
|
+
:ftp => {
|
24
|
+
:port => 21,
|
25
|
+
:passive => false,
|
26
|
+
:filters => ['/'], # dirs
|
27
|
+
},
|
28
|
+
:amzn_s3 => {
|
29
|
+
:filters => [''], # prefixes
|
30
|
+
},
|
31
|
+
:goog_cs => {
|
32
|
+
:filters => [''], # prefixes
|
33
|
+
},
|
34
|
+
:rax_cf => {
|
35
|
+
:endpoint => :us,
|
36
|
+
:region => :dfw,
|
37
|
+
:filters => [''], # prefixes
|
38
|
+
},
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :config
|
43
|
+
attr_reader :running
|
44
|
+
|
45
|
+
def initialize(eternal = false, opts = {})
|
46
|
+
@eternal = eternal
|
47
|
+
|
48
|
+
@logger = opts[:logger] || Logger.new(nil)
|
49
|
+
|
50
|
+
@config = OpenStruct.new(self.class.config_defaults)
|
51
|
+
|
52
|
+
@running = false
|
53
|
+
@threads = {}
|
54
|
+
end
|
55
|
+
|
56
|
+
def configure
|
57
|
+
yield @config
|
58
|
+
|
59
|
+
@logger.level = Logger.const_get(@config.log_level)
|
60
|
+
|
61
|
+
Redis.current = Redis::Namespace.new(@config.redis_ns_ryespy,
|
62
|
+
:redis => Redis.connect(:url => @config.redis_url)
|
63
|
+
)
|
64
|
+
|
65
|
+
@logger.debug { "Configured #{@config.to_s}" }
|
66
|
+
end
|
67
|
+
|
68
|
+
def notifiers
|
69
|
+
unless @notifiers
|
70
|
+
@notifiers = []
|
71
|
+
|
72
|
+
@config.notifiers[:sidekiq].each do |notifier_url|
|
73
|
+
@notifiers << Notifier::Sidekiq.new(
|
74
|
+
:url => notifier_url,
|
75
|
+
:namespace => @config.redis_ns_notifiers,
|
76
|
+
:logger => @logger
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
@notifiers
|
82
|
+
end
|
83
|
+
|
84
|
+
def start
|
85
|
+
begin
|
86
|
+
@running = true
|
87
|
+
|
88
|
+
setup
|
89
|
+
|
90
|
+
@threads[:refresh] ||= Thread.new do
|
91
|
+
refresh_loop # refresh frequently
|
92
|
+
end
|
93
|
+
|
94
|
+
@threads.values.each(&:join)
|
95
|
+
ensure
|
96
|
+
cleanup
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def stop
|
101
|
+
@running = false
|
102
|
+
|
103
|
+
@threads.values.each { |t| t.run if t.status == 'sleep' }
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def setup
|
109
|
+
require_relative "listener/#{@config.listener}"
|
110
|
+
end
|
111
|
+
|
112
|
+
def cleanup
|
113
|
+
end
|
114
|
+
|
115
|
+
def refresh_loop
|
116
|
+
while @running do
|
117
|
+
begin
|
118
|
+
check_all
|
119
|
+
rescue StandardError => e
|
120
|
+
@logger.error { e.to_s }
|
121
|
+
|
122
|
+
raise if @config.log_level == :DEBUG
|
123
|
+
end
|
124
|
+
|
125
|
+
if !@eternal
|
126
|
+
stop
|
127
|
+
|
128
|
+
break
|
129
|
+
end
|
130
|
+
|
131
|
+
@logger.debug { "Snoring for #{@config.polling_interval} s" }
|
132
|
+
|
133
|
+
sleep @config.polling_interval # sleep awhile (snore)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def check_all
|
138
|
+
listener_class_map = {
|
139
|
+
:imap => :IMAP,
|
140
|
+
:ftp => :FTP,
|
141
|
+
:amzn_s3 => :AmznS3,
|
142
|
+
:goog_cs => :GoogCS,
|
143
|
+
:rax_cf => :RaxCF,
|
144
|
+
}
|
145
|
+
|
146
|
+
listener_config = @config[@config.listener].merge({
|
147
|
+
:notifiers => notifiers,
|
148
|
+
:logger => @logger,
|
149
|
+
})
|
150
|
+
|
151
|
+
listener_class = Listener.const_get(listener_class_map[@config.listener])
|
152
|
+
|
153
|
+
listener_class.new(listener_config) do |listener|
|
154
|
+
listener_config[:filters].each { |f| listener.check(f) }
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'fog'
|
2
|
+
|
3
|
+
require_relative 'fogable'
|
4
|
+
|
5
|
+
|
6
|
+
module Ryespy
|
7
|
+
module Listener
|
8
|
+
class AmznS3 < Base
|
9
|
+
|
10
|
+
include Listener::Fogable
|
11
|
+
|
12
|
+
REDIS_KEY_PREFIX = 'amzn_s3'.freeze
|
13
|
+
SIDEKIQ_JOB_CLASS = 'RyespyAmznS3Job'.freeze
|
14
|
+
|
15
|
+
def initialize(opts = {})
|
16
|
+
@config = {
|
17
|
+
:access_key => opts[:access_key],
|
18
|
+
:secret_key => opts[:secret_key],
|
19
|
+
:directory => opts[:bucket],
|
20
|
+
}
|
21
|
+
|
22
|
+
super(opts)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def connect_service
|
28
|
+
@fog_storage = Fog::Storage.new({
|
29
|
+
:provider => 'AWS',
|
30
|
+
:aws_access_key_id => @config[:access_key],
|
31
|
+
:aws_secret_access_key => @config[:secret_key],
|
32
|
+
})
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'redis'
|
3
|
+
|
4
|
+
|
5
|
+
module Ryespy
|
6
|
+
module Listener
|
7
|
+
class Base
|
8
|
+
|
9
|
+
def initialize(opts = {})
|
10
|
+
@notifiers = opts[:notifiers] || []
|
11
|
+
@logger = opts[:logger] || Logger.new(nil)
|
12
|
+
|
13
|
+
@redis = Redis.current
|
14
|
+
|
15
|
+
connect_service
|
16
|
+
|
17
|
+
if block_given?
|
18
|
+
yield self
|
19
|
+
|
20
|
+
close
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def close
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def connect_service
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Ryespy
|
2
|
+
module Listener
|
3
|
+
module Fogable
|
4
|
+
|
5
|
+
def check(prefix)
|
6
|
+
@logger.debug { "prefix: #{prefix}" }
|
7
|
+
|
8
|
+
@logger.debug { "redis_key: #{redis_key}" }
|
9
|
+
|
10
|
+
seen_files = @redis.hgetall(redis_key)
|
11
|
+
|
12
|
+
unseen_files = get_unseen_files(prefix, seen_files)
|
13
|
+
|
14
|
+
@logger.debug { "unseen_files: #{unseen_files}" }
|
15
|
+
|
16
|
+
unseen_files.each do |filename, checksum|
|
17
|
+
@redis.hset(redis_key, filename, checksum)
|
18
|
+
|
19
|
+
# prefix is not included as it is part of key, and list operations
|
20
|
+
# return files (virtually) recursively. Constructing Redis key in this
|
21
|
+
# way means a file matching multiple prefixes will only notify once.
|
22
|
+
@notifiers.each do |notifier|
|
23
|
+
notifier.notify(self.class::SIDEKIQ_JOB_CLASS, [filename])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
@logger.info { "#{prefix}* has #{unseen_files.count} new files" }
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def redis_key
|
33
|
+
[
|
34
|
+
self.class::REDIS_KEY_PREFIX,
|
35
|
+
@config[:directory],
|
36
|
+
].join(':')
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_unseen_files(prefix, seen_files)
|
40
|
+
files = {}
|
41
|
+
|
42
|
+
@fog_storage.directories.get(@config[:directory],
|
43
|
+
:prefix => prefix
|
44
|
+
).files.each do |file|
|
45
|
+
if file.content_type == 'application/directory' || file.content_length == 0
|
46
|
+
next # virtual dirs or 0-length file
|
47
|
+
end
|
48
|
+
|
49
|
+
if seen_files[file.key] != file.etag # etag is server-side checksum
|
50
|
+
files[file.key] = file.etag
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
files
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'net/ftp'
|
2
|
+
|
3
|
+
|
4
|
+
module Ryespy
|
5
|
+
module Listener
|
6
|
+
class FTP < Base
|
7
|
+
|
8
|
+
REDIS_KEY_PREFIX = 'ftp'.freeze
|
9
|
+
SIDEKIQ_JOB_CLASS = 'RyespyFTPJob'.freeze
|
10
|
+
|
11
|
+
def initialize(opts = {})
|
12
|
+
@ftp_config = {
|
13
|
+
:host => opts[:host],
|
14
|
+
:port => opts[:port],
|
15
|
+
:passive => opts[:passive],
|
16
|
+
:username => opts[:username],
|
17
|
+
:password => opts[:password],
|
18
|
+
}
|
19
|
+
|
20
|
+
super(opts)
|
21
|
+
end
|
22
|
+
|
23
|
+
def close
|
24
|
+
@ftp.close
|
25
|
+
end
|
26
|
+
|
27
|
+
def check(dir)
|
28
|
+
@logger.debug { "dir: #{dir}" }
|
29
|
+
|
30
|
+
@logger.debug { "redis_key: #{redis_key(dir)}" }
|
31
|
+
|
32
|
+
seen_files = @redis.hgetall(redis_key(dir))
|
33
|
+
|
34
|
+
unseen_files = get_unseen_files(dir, seen_files)
|
35
|
+
|
36
|
+
@logger.debug { "unseen_files: #{unseen_files}" }
|
37
|
+
|
38
|
+
unseen_files.each do |filename, checksum|
|
39
|
+
@redis.hset(redis_key(dir), filename, checksum)
|
40
|
+
|
41
|
+
@notifiers.each { |n| n.notify(SIDEKIQ_JOB_CLASS, [dir, filename]) }
|
42
|
+
end
|
43
|
+
|
44
|
+
@logger.info { "#{dir} has #{unseen_files.count} new files" }
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def connect_service
|
50
|
+
@ftp = Net::FTP.new
|
51
|
+
|
52
|
+
@ftp.connect(@ftp_config[:host], @ftp_config[:port])
|
53
|
+
|
54
|
+
@ftp.passive = @ftp_config[:passive]
|
55
|
+
|
56
|
+
@ftp.login(@ftp_config[:username], @ftp_config[:password])
|
57
|
+
end
|
58
|
+
|
59
|
+
def redis_key(dir)
|
60
|
+
[
|
61
|
+
REDIS_KEY_PREFIX,
|
62
|
+
@ftp_config[:host],
|
63
|
+
@ftp_config[:port],
|
64
|
+
@ftp_config[:username],
|
65
|
+
dir,
|
66
|
+
].join(':')
|
67
|
+
end
|
68
|
+
|
69
|
+
def get_unseen_files(dir, seen_files)
|
70
|
+
@ftp.chdir(dir)
|
71
|
+
|
72
|
+
files = {}
|
73
|
+
|
74
|
+
@ftp.nlst.each do |file|
|
75
|
+
mtime = @ftp.mtime(file).to_i
|
76
|
+
size = @ftp.size(file) rescue nil # ignore non-file error
|
77
|
+
|
78
|
+
if size # exclude directories
|
79
|
+
checksum = "#{mtime},#{size}".freeze
|
80
|
+
|
81
|
+
if seen_files[file] != checksum
|
82
|
+
files[file] = checksum
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
files
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'fog'
|
2
|
+
|
3
|
+
require_relative 'fogable'
|
4
|
+
|
5
|
+
|
6
|
+
module Ryespy
|
7
|
+
module Listener
|
8
|
+
class GoogCS < Base
|
9
|
+
|
10
|
+
include Listener::Fogable
|
11
|
+
|
12
|
+
REDIS_KEY_PREFIX = 'goog_cs'.freeze
|
13
|
+
SIDEKIQ_JOB_CLASS = 'RyespyGoogCSJob'.freeze
|
14
|
+
|
15
|
+
def initialize(opts = {})
|
16
|
+
@config = {
|
17
|
+
:access_key => opts[:access_key],
|
18
|
+
:secret_key => opts[:secret_key],
|
19
|
+
:directory => opts[:bucket],
|
20
|
+
}
|
21
|
+
|
22
|
+
super(opts)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def connect_service
|
28
|
+
@fog_storage = Fog::Storage.new({
|
29
|
+
:provider => 'Google',
|
30
|
+
:google_storage_access_key_id => @config[:access_key],
|
31
|
+
:google_storage_secret_access_key => @config[:secret_key],
|
32
|
+
})
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|