rest-ftp-daemon 0.30.1 → 0.41
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/Gemfile.lock +11 -4
- data/README.md +30 -6
- data/bin/rest-ftp-daemon +16 -9
- data/config.ru +9 -1
- data/lib/rest-ftp-daemon.rb +13 -1
- data/lib/rest-ftp-daemon/api/defaults.rb +58 -0
- data/lib/rest-ftp-daemon/api/jobs.rb +151 -0
- data/lib/rest-ftp-daemon/api/root.rb +79 -0
- data/lib/rest-ftp-daemon/common.rb +49 -0
- data/lib/rest-ftp-daemon/config.rb +9 -2
- data/lib/rest-ftp-daemon/exceptions.rb +6 -1
- data/lib/rest-ftp-daemon/job.rb +69 -23
- data/lib/rest-ftp-daemon/job_queue.rb +105 -0
- data/lib/rest-ftp-daemon/logger.rb +7 -0
- data/lib/rest-ftp-daemon/notification.rb +80 -0
- data/lib/rest-ftp-daemon/worker_pool.rb +63 -0
- data/lib/rest-ftp-daemon/www.rb +6 -0
- data/rest-ftp-daemon.gemspec +2 -2
- metadata +15 -7
- data/lib/rest-ftp-daemon/server.rb +0 -237
@@ -0,0 +1,49 @@
|
|
1
|
+
module RestFtpDaemon
|
2
|
+
|
3
|
+
class Common
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
# Logger
|
9
|
+
@logger = ActiveSupport::Logger.new APP_LOGTO, 'daily'
|
10
|
+
end
|
11
|
+
|
12
|
+
def id
|
13
|
+
end
|
14
|
+
|
15
|
+
def progname
|
16
|
+
end
|
17
|
+
|
18
|
+
def info message, level = 0
|
19
|
+
# progname = "Job [#{id}]" unless id.nil?
|
20
|
+
# progname = "Worker [#{id}]" unless worker_id.nil?
|
21
|
+
@logger.add(Logger::INFO, "#{' '*(level+1)} #{message}", progname)
|
22
|
+
end
|
23
|
+
|
24
|
+
def notify signal, error = 0, status = {}
|
25
|
+
# Skip is not callback URL defined
|
26
|
+
url = get :notify
|
27
|
+
if url.nil?
|
28
|
+
info "Skipping notification (no valid url provided) sig[#{signal}] e[#{error}] s#{status.inspect}"
|
29
|
+
return
|
30
|
+
end
|
31
|
+
|
32
|
+
# Build notification
|
33
|
+
n = RestFtpDaemon::Notification.new
|
34
|
+
n.job_id = id
|
35
|
+
n.url = url
|
36
|
+
n.signal = signal
|
37
|
+
n.error = error.inspect
|
38
|
+
n.status = status
|
39
|
+
|
40
|
+
# Now, send the notification
|
41
|
+
info "Queuing notification key[#{n.key}] sig[#{signal}] url[#{url}]"
|
42
|
+
Thread.new(n) do |thread|
|
43
|
+
n.notify
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -1,8 +1,15 @@
|
|
1
1
|
module RestFtpDaemon
|
2
2
|
# Global config
|
3
|
-
|
3
|
+
NAME = 'rest-ftp-daemon'
|
4
|
+
VERSION = "0.41"
|
5
|
+
PORT = 3000
|
4
6
|
|
5
7
|
# Transfer config
|
6
8
|
TRANSFER_CHUNK_SIZE = 100000
|
7
|
-
THREAD_SLEEP_BEFORE_DIE =
|
9
|
+
THREAD_SLEEP_BEFORE_DIE = 10
|
10
|
+
|
11
|
+
# Logging
|
12
|
+
APP_LOGTO = "/tmp/#{NAME}.log"
|
13
|
+
LOG_TRIM_PROGNAME = 18
|
14
|
+
|
8
15
|
end
|
@@ -16,6 +16,11 @@ module RestFtpDaemon
|
|
16
16
|
class JobSourceNotFound < RestFtpDaemonException; end
|
17
17
|
class JobTargetMissing < RestFtpDaemonException; end
|
18
18
|
class JobTargetUnparseable < RestFtpDaemonException; end
|
19
|
-
class JobTargetPermission < RestFtpDaemonException; end
|
19
|
+
#class JobTargetPermission < RestFtpDaemonException; end
|
20
|
+
class JobTargetFileExists < RestFtpDaemonException; end
|
21
|
+
|
22
|
+
class NotificationMissingUrl < RestFtpDaemonException; end
|
23
|
+
class NotificationMissingSignal < RestFtpDaemonException; end
|
24
|
+
|
20
25
|
|
21
26
|
end
|
data/lib/rest-ftp-daemon/job.rb
CHANGED
@@ -1,25 +1,50 @@
|
|
1
1
|
module RestFtpDaemon
|
2
|
-
class Job
|
2
|
+
class Job < RestFtpDaemon::Common
|
3
3
|
|
4
4
|
def initialize(id, params={})
|
5
|
+
# Call super
|
6
|
+
super()
|
7
|
+
|
5
8
|
# Grab params
|
6
9
|
@params = params
|
7
10
|
@target = nil
|
8
11
|
@source = nil
|
9
12
|
|
13
|
+
# Logger
|
14
|
+
#@logger = ActiveSupport::Logger.new APP_LOGTO, 'daily'
|
15
|
+
|
10
16
|
# Init context
|
11
17
|
set :id, id
|
12
18
|
set :started_at, Time.now
|
13
19
|
set :status, :initialized
|
20
|
+
|
21
|
+
# Send first notification
|
22
|
+
notify "rftpd.queued"
|
23
|
+
end
|
24
|
+
|
25
|
+
# def job_id
|
26
|
+
# get :id
|
27
|
+
# end
|
28
|
+
|
29
|
+
def progname
|
30
|
+
job_id = get(:id)
|
31
|
+
"JOB #{job_id}"
|
14
32
|
end
|
15
33
|
|
16
34
|
def id
|
17
|
-
|
35
|
+
get :id
|
36
|
+
end
|
37
|
+
|
38
|
+
def priority
|
39
|
+
get :priority
|
40
|
+
end
|
41
|
+
def get_status
|
42
|
+
get :status
|
18
43
|
end
|
19
44
|
|
20
45
|
def process
|
21
46
|
# Init
|
22
|
-
"process
|
47
|
+
info "Job.process starting"
|
23
48
|
set :status, :starting
|
24
49
|
set :error, 0
|
25
50
|
|
@@ -30,11 +55,13 @@ module RestFtpDaemon
|
|
30
55
|
# Process
|
31
56
|
transfer
|
32
57
|
|
33
|
-
rescue Net::FTPPermError
|
58
|
+
rescue Net::FTPPermError => exception
|
59
|
+
info "Job.process failed [Net::FTPPermError]"
|
34
60
|
set :status, :failed
|
35
61
|
set :error, exception.class
|
36
62
|
|
37
63
|
rescue RestFtpDaemonException => exception
|
64
|
+
info "Job.process failed [RestFtpDaemonException::#{exception.class}]"
|
38
65
|
set :status, :failed
|
39
66
|
set :error, exception.class
|
40
67
|
|
@@ -43,8 +70,14 @@ module RestFtpDaemon
|
|
43
70
|
# set :error, exception.class
|
44
71
|
|
45
72
|
else
|
73
|
+
info "Job.process finished"
|
74
|
+
# set :error, 0
|
75
|
+
#set :status, :wandering
|
76
|
+
|
77
|
+
# Wait for a few seconds before marking the job as finished
|
78
|
+
# info "#{prefix} wander for #{RestFtpDaemon::THREAD_SLEEP_BEFORE_DIE} sec"
|
79
|
+
# wander RestFtpDaemon::THREAD_SLEEP_BEFORE_DIE
|
46
80
|
set :status, :finished
|
47
|
-
set :error, 0
|
48
81
|
end
|
49
82
|
|
50
83
|
end
|
@@ -62,12 +95,6 @@ module RestFtpDaemon
|
|
62
95
|
@params
|
63
96
|
end
|
64
97
|
|
65
|
-
def wander time
|
66
|
-
@wander_for = time
|
67
|
-
@wander_started = Time.now
|
68
|
-
sleep time
|
69
|
-
end
|
70
|
-
|
71
98
|
def status text
|
72
99
|
@status = text
|
73
100
|
end
|
@@ -79,20 +106,33 @@ module RestFtpDaemon
|
|
79
106
|
Time.now - @params[:started_at]
|
80
107
|
end
|
81
108
|
|
109
|
+
def wander time
|
110
|
+
info "Job.wander #{time}"
|
111
|
+
@wander_for = time
|
112
|
+
@wander_started = Time.now
|
113
|
+
sleep time
|
114
|
+
info "Job.wandered ok"
|
115
|
+
end
|
116
|
+
|
82
117
|
def wandering_time
|
83
118
|
return if @wander_started.nil? || @wander_for.nil?
|
84
119
|
@wander_for.to_f - (Time.now - @wander_started)
|
85
120
|
end
|
86
121
|
|
87
|
-
def exception_handler(actor, reason)
|
88
|
-
|
89
|
-
|
90
|
-
end
|
122
|
+
# def exception_handler(actor, reason)
|
123
|
+
# set :status, :crashed
|
124
|
+
# set :error, reason
|
125
|
+
# end
|
91
126
|
|
92
127
|
def set attribute, value
|
93
128
|
return unless @params.is_a? Enumerable
|
94
129
|
@params[:updated_at] = Time.now
|
95
|
-
@params[attribute] = value
|
130
|
+
@params[attribute.to_s] = value
|
131
|
+
end
|
132
|
+
|
133
|
+
def get attribute
|
134
|
+
return nil unless @params.is_a? Enumerable
|
135
|
+
@params[attribute.to_s]
|
96
136
|
end
|
97
137
|
|
98
138
|
def prepare
|
@@ -124,8 +164,9 @@ module RestFtpDaemon
|
|
124
164
|
end
|
125
165
|
|
126
166
|
def transfer
|
127
|
-
#
|
167
|
+
# Send first notification
|
128
168
|
transferred = 0
|
169
|
+
notify "rftpd.started"
|
129
170
|
|
130
171
|
# Ensure @source and @target are there
|
131
172
|
set :status, :checking_source
|
@@ -142,15 +183,19 @@ module RestFtpDaemon
|
|
142
183
|
set :status, :checking_target
|
143
184
|
ftp = Net::FTP.new(@target.host)
|
144
185
|
ftp.passive = true
|
145
|
-
ftp.login
|
186
|
+
ftp.login @target.user, @target.password
|
146
187
|
ftp.chdir(target_path)
|
147
188
|
|
148
189
|
# Check for target file presence
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
190
|
+
if get(:overwrite).nil?
|
191
|
+
results = ftp.list(target_name)
|
192
|
+
#results = ftp.list()
|
193
|
+
|
194
|
+
unless results.count.zero?
|
195
|
+
ftp.close
|
196
|
+
notify "rftpd.ended", RestFtpDaemon::JobTargetFileExists
|
197
|
+
raise RestFtpDaemon::JobTargetFileExists
|
198
|
+
end
|
154
199
|
end
|
155
200
|
|
156
201
|
# Do transfer
|
@@ -166,6 +211,7 @@ module RestFtpDaemon
|
|
166
211
|
end
|
167
212
|
|
168
213
|
# Close FTP connexion
|
214
|
+
notify "rftpd.ended"
|
169
215
|
ftp.close
|
170
216
|
end
|
171
217
|
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module RestFtpDaemon
|
4
|
+
class JobQueue < Queue
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@queued = []
|
8
|
+
@popped = []
|
9
|
+
|
10
|
+
@waiting = []
|
11
|
+
@queued.taint # enable tainted communication
|
12
|
+
@waiting.taint
|
13
|
+
self.taint
|
14
|
+
@mutex = Mutex.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def queued
|
18
|
+
@queued
|
19
|
+
end
|
20
|
+
def queued_size
|
21
|
+
@queued.length
|
22
|
+
end
|
23
|
+
|
24
|
+
def popped
|
25
|
+
@popped
|
26
|
+
end
|
27
|
+
def popped_size
|
28
|
+
@popped.length
|
29
|
+
end
|
30
|
+
|
31
|
+
def all
|
32
|
+
@queued + @popped
|
33
|
+
end
|
34
|
+
def all_size
|
35
|
+
popped_size + queued_size
|
36
|
+
end
|
37
|
+
|
38
|
+
def push(obj)
|
39
|
+
# Check that itme responds to "priorty" method
|
40
|
+
raise "object should respond to priority method" unless obj.respond_to? :priority
|
41
|
+
|
42
|
+
@mutex.synchronize{
|
43
|
+
@queued.push obj
|
44
|
+
begin
|
45
|
+
t = @waiting.shift
|
46
|
+
t.wakeup if t
|
47
|
+
rescue ThreadError
|
48
|
+
retry
|
49
|
+
end
|
50
|
+
}
|
51
|
+
end
|
52
|
+
alias << push
|
53
|
+
alias enq push
|
54
|
+
|
55
|
+
#
|
56
|
+
# Retrieves data from the queue. If the queue is empty, the calling thread is
|
57
|
+
# suspended until data is pushed onto the queue. If +non_block+ is true, the
|
58
|
+
# thread isn't suspended, and an exception is raised.
|
59
|
+
#
|
60
|
+
def pop(non_block=false)
|
61
|
+
@mutex.synchronize{
|
62
|
+
while true
|
63
|
+
if @queued.empty?
|
64
|
+
raise ThreadError, "queue empty" if non_block
|
65
|
+
@waiting.push Thread.current
|
66
|
+
@mutex.sleep
|
67
|
+
else
|
68
|
+
return pick
|
69
|
+
end
|
70
|
+
end
|
71
|
+
}
|
72
|
+
end
|
73
|
+
alias shift pop
|
74
|
+
alias deq pop
|
75
|
+
|
76
|
+
def empty?
|
77
|
+
@queued.empty?
|
78
|
+
end
|
79
|
+
|
80
|
+
def clear
|
81
|
+
@queued.clear
|
82
|
+
end
|
83
|
+
|
84
|
+
def num_waiting
|
85
|
+
@waiting.size
|
86
|
+
end
|
87
|
+
|
88
|
+
protected
|
89
|
+
|
90
|
+
def pick
|
91
|
+
# Sort jobs by priority and get the biggest one
|
92
|
+
picked = @queued.sort { |a,b| a.priority.to_i <=> b.priority.to_i }.last
|
93
|
+
|
94
|
+
# Delete it from the queue
|
95
|
+
@queued.delete_if { |item| item == picked } unless picked.nil?
|
96
|
+
|
97
|
+
# Stack it to popped items
|
98
|
+
@popped.push picked
|
99
|
+
|
100
|
+
# Return picked
|
101
|
+
picked
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module RestFtpDaemon
|
2
|
+
class Notification < RestFtpDaemon::Common
|
3
|
+
attr_accessor :job_id
|
4
|
+
attr_accessor :signal
|
5
|
+
attr_accessor :error
|
6
|
+
attr_accessor :message
|
7
|
+
attr_accessor :status
|
8
|
+
attr_accessor :url
|
9
|
+
attr_accessor :job
|
10
|
+
attr_accessor :key
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
#def initialize(job, signal, error, status)
|
14
|
+
# Grab params
|
15
|
+
#@job = job
|
16
|
+
# @signal = signal
|
17
|
+
# @error = error
|
18
|
+
# @status = status
|
19
|
+
@status = {}
|
20
|
+
@error = 0
|
21
|
+
@message = nil
|
22
|
+
|
23
|
+
# Generate a random key
|
24
|
+
@key = SecureRandom.hex(2)
|
25
|
+
|
26
|
+
# Call super
|
27
|
+
super
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
def progname
|
32
|
+
"NOTIF #{@key}"
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# def status key, val
|
37
|
+
# @status[key.to_s] = val
|
38
|
+
# end
|
39
|
+
|
40
|
+
def notify
|
41
|
+
# Check context
|
42
|
+
raise NotificationMissingUrl unless @url
|
43
|
+
raise NotificationMissingSignal unless @signal
|
44
|
+
#sraise NotifImpossible unless @status
|
45
|
+
|
46
|
+
# Params
|
47
|
+
params = {
|
48
|
+
id: @job_id,
|
49
|
+
host: get_hostname,
|
50
|
+
signal: @signal,
|
51
|
+
error: @error,
|
52
|
+
}
|
53
|
+
|
54
|
+
# Add status only if present
|
55
|
+
params["status"] = @status unless @status.empty?
|
56
|
+
params["message"] = @message unless @message.to_s.blank?
|
57
|
+
|
58
|
+
# Log this notification
|
59
|
+
info "notify params: #{params.inspect}"
|
60
|
+
|
61
|
+
# Prepare query
|
62
|
+
uri = URI(@url)
|
63
|
+
headers = {"Content-Type" => "application/json",
|
64
|
+
"Accept" => "application/json"}
|
65
|
+
|
66
|
+
# Post the notification
|
67
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
68
|
+
response = http.post(uri.path, params.to_json, headers)
|
69
|
+
|
70
|
+
info "notify reply: #{response.body.strip}"
|
71
|
+
end
|
72
|
+
|
73
|
+
protected
|
74
|
+
|
75
|
+
def get_hostname
|
76
|
+
`hostname`.chomp
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|