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.
@@ -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/notifiers/sidekiq'
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
- module Ryespy
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'
@@ -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