rest-ftp-daemon 0.20.0 → 0.30.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.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- ODc0NWU3ZTFkMDMxNzJkYzNmZmZhNzRlODZlYzdiNDI4Y2NlMWE2Ng==
4
+ YThkNmNkZTEwNzM2YzcwM2I2N2NiOWE5OTQwNDI1N2M0NTFjZWZhOA==
5
5
  data.tar.gz: !binary |-
6
- NmM2NTA1MDViYzU1MDc0MDViNTI0MWY2NDQ1NjAzY2FkNDcxMTA1Yw==
6
+ ZTllMzMyNGMzNTE1M2U5ZTBjZTc3MWJmYWNiYWJlNzQwM2NiNmMxNQ==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- MDBhM2E0YmQyNjMzM2Y2MGE5OTBiOTA4YmI2Y2NiYTI1MjYxZmMwOWEyMjlk
10
- MzQ4NTk0N2FiMDA3NWQ1N2Y0YzJkZGRkZWUyZDRhZDEzODAwOWRiNDRmMDI3
11
- ZTVhMTY2ZDkwMDg3YTAxODQ5NWY5MzcwZGM2N2QyZjQ0MDJlYWM=
9
+ ODQzMzc5MzRiZDZjZDAyM2QxZWQyMThjNTUyZmNlZWJhOGFiNTJiODAyYWJi
10
+ YjExZmZiNGMzMDM5ZDRjMmRjYTMxMTNhZGU0ZWNkODMxMzVjZTQ3YWY2MTk5
11
+ MjYxM2MwNDU5YzM2OWU1MGVjZjc5NTgwYWVjOTdmZTEzNjQ5MGM=
12
12
  data.tar.gz: !binary |-
13
- MTA1ZmRkMGYzM2VkMWExMDIwMDVjZDhkZWFhOGI1YzYxMGNjNzViNzQxNzdk
14
- YjIzYWU0ZmIyYzZjNTRlYTkwMTY0YTAxMDUwMWU1N2Q5MWFhNDgwNTM3OGNl
15
- ODJmODA4MDZiMjhlOWRmOTY2M2IxMDFhYzdiYzgxMTNhOGRlYzU=
13
+ NTBhZWM0NDM2ODNiY2ZjNTA0YjYxYWE3MTBlYWUwZmM3NjgxOGZhNmNlZDlk
14
+ ZGEwOWE1ZDM4ZjU1N2FjYTQ2ZTNlMjllOGE5ODlhMTJjNjA5ZGFhYjFlODdj
15
+ ZDBjM2Y1ZTVmODdiYTk0YjdkMGViMGE1ZDA4ZjQ0NzdhZWUxODE=
data/Gemfile CHANGED
@@ -1,11 +1,3 @@
1
1
  source "http://rubygems.org"
2
2
 
3
- gem "sinatra"
4
- gem "json"
5
- # gem "thin"
6
-
7
- group :development do
8
- # gem "shoulda", ">= 0"
9
- gem "bundler", "~> 1.0"
10
- # gem "jeweler", "~> 2.0.1"
11
- end
3
+ gemspec
data/Gemfile.lock CHANGED
@@ -1,20 +1,65 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rest-ftp-daemon (0.22)
5
+ grape
6
+ json
7
+
1
8
  GEM
2
9
  remote: http://rubygems.org/
3
10
  specs:
11
+ activesupport (4.1.5)
12
+ i18n (~> 0.6, >= 0.6.9)
13
+ json (~> 1.7, >= 1.7.7)
14
+ minitest (~> 5.1)
15
+ thread_safe (~> 0.1)
16
+ tzinfo (~> 1.1)
17
+ axiom-types (0.1.1)
18
+ descendants_tracker (~> 0.0.4)
19
+ ice_nine (~> 0.11.0)
20
+ thread_safe (~> 0.3, >= 0.3.1)
21
+ builder (3.2.2)
22
+ coercible (1.0.0)
23
+ descendants_tracker (~> 0.0.1)
24
+ descendants_tracker (0.0.4)
25
+ thread_safe (~> 0.3, >= 0.3.1)
26
+ equalizer (0.0.9)
27
+ grape (0.8.0)
28
+ activesupport
29
+ builder
30
+ hashie (>= 2.1.0)
31
+ multi_json (>= 1.3.2)
32
+ multi_xml (>= 0.5.2)
33
+ rack (>= 1.3.0)
34
+ rack-accept
35
+ rack-mount
36
+ virtus (>= 1.0.0)
37
+ hashie (3.2.0)
38
+ i18n (0.6.11)
39
+ ice_nine (0.11.0)
4
40
  json (1.8.1)
41
+ minitest (5.4.0)
42
+ multi_json (1.10.1)
43
+ multi_xml (0.5.5)
5
44
  rack (1.5.2)
6
- rack-protection (1.5.3)
7
- rack
8
- sinatra (1.4.5)
9
- rack (~> 1.4)
10
- rack-protection (~> 1.4)
11
- tilt (~> 1.3, >= 1.3.4)
12
- tilt (1.4.1)
45
+ rack-accept (0.4.5)
46
+ rack (>= 0.4)
47
+ rack-mount (0.8.3)
48
+ rack (>= 1.0.0)
49
+ rake (10.3.2)
50
+ thread_safe (0.3.4)
51
+ tzinfo (1.2.2)
52
+ thread_safe (~> 0.1)
53
+ virtus (1.0.3)
54
+ axiom-types (~> 0.1)
55
+ coercible (~> 1.0)
56
+ descendants_tracker (~> 0.0, >= 0.0.3)
57
+ equalizer (~> 0.0, >= 0.0.9)
13
58
 
14
59
  PLATFORMS
15
60
  ruby
16
61
 
17
62
  DEPENDENCIES
18
- bundler (~> 1.0)
19
- json
20
- sinatra
63
+ bundler (~> 1.6)
64
+ rake
65
+ rest-ftp-daemon!
data/README.md CHANGED
@@ -14,12 +14,13 @@ As of today, its main features are :
14
14
  Installation
15
15
  ------------------------------------------------------------------------------------
16
16
 
17
- This project is available as a rubygem, requires on ruby >= 1.9 and rubygems installed.
17
+ This project is available as a rubygem, requires on ruby >= 1.9.3 and rubygems installed.
18
18
 
19
19
  Get and install the gem from rubygems.org:
20
20
 
21
21
  ```
22
- gem install rest-ftp-daemon
22
+ # apt-get install ruby2.1 rubygems
23
+ gem install rest-ftp-daemon --no-ri --no-rdoc
23
24
  ```
24
25
 
25
26
  Start the daemon:
@@ -40,11 +41,11 @@ For now, daemon logs to ```APP_LOGTO``` defined in ```lib/config.rb```
40
41
  Usage examples
41
42
  ------------------------------------------------------------------------------------
42
43
 
43
- Start a job to transfer a file named "file.ova" to a local FTP server
44
+ Start a job to transfer a file named "file.iso" to a local FTP server
44
45
 
45
46
  ```
46
47
  curl -H "Content-Type: application/json" -X POST -D /dev/stdout -d \
47
- '{"source":"~/file.ova","target":"ftp://anonymous@localhost/incoming/dest2.ova"}' "http://localhost:3000/jobs"
48
+ '{"source":"~/file.iso","target":"ftp://anonymous@localhost/incoming/dest2.iso"}' "http://localhost:3000/jobs"
48
49
  ```
49
50
 
50
51
  Start a job to transfer a file named "file.dmg" to a local FTP server
data/bin/rest-ftp-daemon CHANGED
@@ -6,17 +6,18 @@ require 'thin'
6
6
  # Initialize some local constants
7
7
  APP_ROOT = File.dirname(__FILE__) + '/../'
8
8
  APP_NAME = 'rest-ftp-daemon'
9
- APP_VER = File.read "#{APP_ROOT}/VERSION"
9
+
10
10
  APP_STARTED = Time.now
11
11
  APP_DEFAULT_PORT = 3000
12
12
  APP_LOGTO = "/tmp/#{APP_NAME}.log"
13
13
 
14
14
  # Prepare thin
15
- rackup_file = File.expand_path "#{APP_ROOT}/lib/config.ru"
15
+ rackup_file = File.expand_path "#{APP_ROOT}/config.ru"
16
16
  argv = ARGV
17
17
  argv << ["-R", rackup_file] unless ARGV.include?("-R")
18
18
  argv << ["-p", APP_DEFAULT_PORT.to_s] unless ARGV.include?("-p")
19
19
  argv << ["-e", "production"] unless ARGV.include?("-e")
20
- argv << ["--stats", "/stats"] unless ARGV.include?("--stats")
20
+ argv << ["--daemonize"] unless ARGV.include?("-R")
21
+ #argv << ["--stats", "/stats"] unless ARGV.include?("--stats")
21
22
 
22
23
  Thin::Runner.new(argv.flatten).run!
data/config.ru ADDED
@@ -0,0 +1,6 @@
1
+ # Load gem files
2
+ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), 'lib'))
3
+ require 'rest-ftp-daemon'
4
+
5
+ # Start REST FTP Daemon
6
+ run RestFtpDaemon::API
@@ -1,309 +1,10 @@
1
- # module RestFtpDaemon
2
-
3
- class RestFtpDaemon < Sinatra::Base
4
-
5
- # General config
6
- configure :development, :production do
7
-
8
- # Create new thread group
9
- @@workers = ThreadGroup.new
10
-
11
- # Logging configuration
12
- #use Rack::CommonLogger, logger
13
-
14
- # Some other configuration
15
- disable :sessions
16
- disable :logging
17
- end
18
-
19
- # Server initialization
20
- def initialize
21
- # Setup logger
22
- @logger = Logger.new(APP_LOGTO, 'daily')
23
- #@logger = Logger.new
24
- #@logger.level = Logger::INFO
25
-
26
- # Other stuff
27
- @@last_worker_id = 0
28
- @@hostname = `hostname`.chomp
29
-
30
- super
31
- end
32
-
33
- # Server global status
34
- get "/" do
35
- # Debug query
36
- info "GET /"
37
-
38
- # Build response
39
- content_type :json
40
- JSON.pretty_generate get_status
41
- end
42
-
43
- # List jobs
44
- get "/jobs" do
45
- # Debug query
46
- info "GET /jobs"
47
-
48
- # Build response
49
- content_type :json
50
- JSON.pretty_generate get_jobs
51
- end
52
-
53
- # Get job info
54
- get "/jobs/:id" do
55
- # Debug query
56
- info "GET /jobs/#{params[:id]}"
57
-
58
- # Find this process by name
59
- found = find_job params[:id]
60
-
61
- # Build response
62
- error 404 and return if found.nil?
63
- content_type :json
64
- JSON.pretty_generate found
65
- end
66
-
67
- # Delete jobs
68
- delete "/jobs/:id" do
69
- # Debug query
70
- info "DELETE /jobs/#{params[:name]}"
71
-
72
- # Find and kill this job
73
- found = delete_job params[:id]
74
-
75
- # Build response
76
- error 404 and return if found.nil?
77
- content_type :json
78
- JSON.pretty_generate found
79
- end
80
-
81
- # Spawn a new thread for this new job
82
- post '/jobs' do
83
- # Extract payload
84
- request.body.rewind
85
- payload = JSON.parse request.body.read
86
-
87
- # Debug query
88
- info "POST /jobs: #{payload.to_json}"
89
-
90
- # Spawn a thread for this job
91
- result = new_job payload
92
-
93
- # Build response
94
- content_type :json
95
- JSON.pretty_generate result
96
- end
97
-
98
- protected
99
-
100
- def process_job
101
- # Init
102
- info "process_job: starting"
103
- job = Thread.current.job
104
- job_status :started
105
- transferred = 0
106
-
107
- # Check source
108
- job_source = File.expand_path(job["source"])
109
- if !(File.exists? job_source)
110
- job_error ERR_JOB_SOURCE_NOTFOUND, :ERR_JOB_SOURCE_NOTFOUND
111
- return
112
- end
113
- info "process_job: job_source: #{job_source}"
114
- source_size = File.size job_source
115
- job_set :source_size, source_size
116
-
117
- # Check target
118
- job_target = job["target"]
119
- target = URI(job_target) rescue nil
120
- if job_target.nil? || target.nil?
121
- job_error ERR_JOB_TARGET_UNPARSEABLE, :ERR_JOB_TARGET_UNPARSEABLE
122
- return
123
- end
124
- info "process_job: job_target: #{job_target}"
125
-
126
- # Split URI
127
- target_path = File.dirname target.path
128
- target_name = File.basename target.path
129
- info "ftp_transfer: job_target.host [#{target.host}]"
130
- info "ftp_transfer: target_path [#{target_path}]"
131
- info "ftp_transfer: target_name [#{target_name}]"
132
-
133
- # Prepare FTP transfer
134
- ftp = Net::FTP.new(target.host)
135
- ftp.passive = true
136
- ftp.login
137
- ftp.chdir(target_path)
138
-
139
-
140
- # Check if target file is found
141
- info "source: checking target file"
142
- job_status :checking_target
143
- job_error ERR_BUSY, :checking_target
144
-
145
- results = ftp.list(target_name)
146
- info "ftp.list: #{results}"
147
- unless results.count.zero?
148
- job_error ERR_JOB_TARGET_PRESENT, :ERR_JOB_TARGET_PRESENT
149
- info "target: existing: ERR_JOB_TARGET_PRESENT"
150
- ftp.close
151
- return
152
- end
153
-
154
-
155
- # Do transfer
156
- info "source: starting stransfer"
157
- #Thread.current[:status] = :transferring
158
- job_status :uploading
159
- job_error ERR_BUSY, :uploading
160
-
161
- begin
162
- ftp.putbinaryfile(job_source, target_name, TRANSFER_CHUNK_SIZE) do |block|
163
- # Update thread info
164
- percent = (100.0 * transferred / source_size).round(1)
165
- job_set :progress, percent
166
- job_set :transferred, transferred
167
- info "transferring [#{percent} %] of [#{target_name}]"
168
-
169
- # Update counters
170
- transferred += TRANSFER_CHUNK_SIZE
171
- end
172
-
173
- rescue Net::FTPPermError
174
- #job_status :failed
175
- job_error ERR_JOB_PERMISSION, :ERR_JOB_PERMISSION
176
- info "source: FAILED: PERMISSIONS ERROR"
177
-
178
- else
179
- #job_status :finished
180
- job_error ERR_OK, :finished
181
- info "source: finished stransfer"
182
- end
183
-
184
- # Close FTP connexion
185
- ftp.close
186
- end
187
-
188
- def get_status
189
- info "> get_status"
190
- {
191
- app_name: APP_NAME,
192
- hostname: @@hostname,
193
- version: APP_VER,
194
- started: APP_STARTED,
195
- uptime: (Time.now - APP_STARTED).round(1),
196
- jobs_count: @@workers.list.count,
197
- }
198
- end
199
-
200
- def get_jobs
201
- info "> get_jobs"
202
-
203
- # Collect info's
204
- @@workers.list.map { |thread| thread.job }
205
- end
206
-
207
- def delete_job id
208
- info "> delete_job(#{id})"
209
-
210
- # Find jobs with this id
211
- jobs = jobs_with_id id
212
-
213
- # Kill them
214
- jobs.each{ |thread| Thread.kill(thread) }
215
-
216
- # Return the first one
217
- return nil if jobs.empty?
218
- jobs.first.job
219
- end
220
-
221
- def find_job id
222
- info "> find_job(#{id})"
223
-
224
- # Find jobs with this id
225
- jobs = jobs_with_id id
226
-
227
- # Return the first one
228
- return nil if jobs.empty?
229
- jobs.first.job
230
- end
231
-
232
- def jobs_with_id id
233
- info "> find_jobs_by_id(#{id})"
234
- @@workers.list.select{ |thread| thread[:id].to_s == id.to_s }
235
- end
236
-
237
- def new_job context = {}
238
- info "new_job"
239
-
240
- # Generate name
241
- @@last_worker_id +=1
242
- host = @@hostname.split('.')[0]
243
- worker_id = @@last_worker_id
244
- worker_name = "#{host}-#{Process.pid.to_s}-#{worker_id}"
245
- info "new_job: creating thread [#{worker_name}]"
246
-
247
- # Parse parameters
248
- job_source = context["source"]
249
- job_target = context["target"]
250
- return { code: ERR_REQ_SOURCE_MISSING, errmsg: :ERR_REQ_SOURCE_MISSING} if job_source.nil?
251
- return { code: ERR_REQ_TARGET_MISSING, errmsg: :ERR_REQ_TARGET_MISSING} if job_target.nil?
252
-
253
- # Parse dest URI
254
- target = URI(job_target)
255
- info target.scheme
256
- return { code: ERR_REQ_TARGET_SCHEME, errmsg: :ERR_REQ_TARGET_SCHEME} unless target.scheme == "ftp"
257
-
258
- # Create thread
259
- job = Thread.new(worker_id, worker_name, job) do
260
- # Tnitialize thread
261
- Thread.abort_on_exception = true
262
- job_status :initializing
263
- job_error ERR_OK
264
-
265
- # Initialize job info
266
- Thread.current[:job] = {}
267
- Thread.current[:job].merge! context if context.is_a? Enumerable
268
- Thread.current[:id] = worker_id
269
- job_set :worker_name, worker_name
270
- job_set :created, Time.now
271
-
272
- # Do the job
273
- info "new_job: thread running"
274
- process_job
275
-
276
- # Sleep a few seconds before dying
277
- job_status :graceful_ending
278
- sleep THREAD_SLEEP_BEFORE_DIE
279
- job_status :ended
280
- info "new_job: thread finished"
281
- end
282
-
283
- # Keep thread in thread group
284
- info "new_job: attaching thread [#{worker_name}] to group"
285
- @@workers.add job
286
-
287
- return { code: 0, errmsg: 'success', worker_id: worker_id, context: context }
288
- end
289
-
290
- def info msg=""
291
- @logger.info msg
292
- end
293
-
294
- def job_error error, errmsg = nil
295
- job_set :error, error
296
- job_set :errmsg, errmsg
297
- end
298
- def job_status status
299
- job_set :status, status
300
- end
301
-
302
- def job_set attribute, value, thread = Thread.current
303
- thread[:job][attribute] = value if thread[:job].is_a? Enumerable
304
- end
305
-
306
-
307
- end
308
-
309
- # end
1
+ # Global libs
2
+ require 'json'
3
+ require 'grape'
4
+ require 'net/ftp'
5
+
6
+ # My libs
7
+ require 'rest-ftp-daemon/config'
8
+ require 'rest-ftp-daemon/exceptions'
9
+ require 'rest-ftp-daemon/job'
10
+ require 'rest-ftp-daemon/server'
@@ -0,0 +1,8 @@
1
+ module RestFtpDaemon
2
+ # Global config
3
+ VERSION = "0.30.1"
4
+
5
+ # Transfer config
6
+ TRANSFER_CHUNK_SIZE = 100000
7
+ THREAD_SLEEP_BEFORE_DIE = 30
8
+ end
@@ -0,0 +1,21 @@
1
+ module RestFtpDaemon
2
+
3
+ class RestFtpDaemonException < StandardError; end
4
+
5
+ class DummyException < RestFtpDaemonException; end
6
+
7
+ class RequestSourceMissing < RestFtpDaemonException; end
8
+ class RequestSourceNotFound < RestFtpDaemonException; end
9
+ class RequestTargetMissing < RestFtpDaemonException; end
10
+ class RequestTargetScheme < RestFtpDaemonException; end
11
+
12
+ class JobPrerequisitesNotMet < RestFtpDaemonException; end
13
+
14
+ class JobNotFound < RestFtpDaemonException; end
15
+ class JobSourceMissing < RestFtpDaemonException; end
16
+ class JobSourceNotFound < RestFtpDaemonException; end
17
+ class JobTargetMissing < RestFtpDaemonException; end
18
+ class JobTargetUnparseable < RestFtpDaemonException; end
19
+ class JobTargetPermission < RestFtpDaemonException; end
20
+
21
+ end
@@ -0,0 +1,173 @@
1
+ module RestFtpDaemon
2
+ class Job
3
+
4
+ def initialize(id, params={})
5
+ # Grab params
6
+ @params = params
7
+ @target = nil
8
+ @source = nil
9
+
10
+ # Init context
11
+ set :id, id
12
+ set :started_at, Time.now
13
+ set :status, :initialized
14
+ end
15
+
16
+ def id
17
+ @params[:id]
18
+ end
19
+
20
+ def process
21
+ # Init
22
+ "process [#{@id}] starting"
23
+ set :status, :starting
24
+ set :error, 0
25
+
26
+ begin
27
+ # Validate job and params
28
+ prepare
29
+
30
+ # Process
31
+ transfer
32
+
33
+ rescue Net::FTPPermError
34
+ set :status, :failed
35
+ set :error, exception.class
36
+
37
+ rescue RestFtpDaemonException => exception
38
+ set :status, :failed
39
+ set :error, exception.class
40
+
41
+ # rescue Exception => exception
42
+ # set :status, :crashed
43
+ # set :error, exception.class
44
+
45
+ else
46
+ set :status, :finished
47
+ set :error, 0
48
+ end
49
+
50
+ end
51
+
52
+ def describe
53
+ # Update realtime info
54
+ w = wandering_time
55
+ set :wandering, w.round(2) unless w.nil?
56
+
57
+ # Update realtime info
58
+ u = up_time
59
+ set :uptime, u.round(2) unless u.nil?
60
+
61
+ # Return the whole structure
62
+ @params
63
+ end
64
+
65
+ def wander time
66
+ @wander_for = time
67
+ @wander_started = Time.now
68
+ sleep time
69
+ end
70
+
71
+ def status text
72
+ @status = text
73
+ end
74
+
75
+ protected
76
+
77
+ def up_time
78
+ return if @params[:started_at].nil?
79
+ Time.now - @params[:started_at]
80
+ end
81
+
82
+ def wandering_time
83
+ return if @wander_started.nil? || @wander_for.nil?
84
+ @wander_for.to_f - (Time.now - @wander_started)
85
+ end
86
+
87
+ def exception_handler(actor, reason)
88
+ set :status, :crashed
89
+ set :error, reason
90
+ end
91
+
92
+ def set attribute, value
93
+ return unless @params.is_a? Enumerable
94
+ @params[:updated_at] = Time.now
95
+ @params[attribute] = value
96
+ end
97
+
98
+ def prepare
99
+ # Init
100
+ set :status, :preparing
101
+
102
+ # Check source
103
+ raise JobSourceMissing unless @params["source"]
104
+ @source = File.expand_path(@params["source"])
105
+ set :debug_source, @source
106
+ raise JobSourceNotFound unless File.exists? @source
107
+
108
+ # Check target
109
+ raise JobTargetMissing unless @params["target"]
110
+ @target = URI(@params["target"]) rescue nil
111
+ set :debug_target, @target.inspect
112
+ raise JobTargetUnparseable if @target.nil?
113
+ end
114
+
115
+ def transfer_fake
116
+ # Init
117
+ set :status, :faking
118
+
119
+ # Work
120
+ (0..9).each do |i|
121
+ set :faking, i
122
+ sleep 0.5
123
+ end
124
+ end
125
+
126
+ def transfer
127
+ # Init
128
+ transferred = 0
129
+
130
+ # Ensure @source and @target are there
131
+ set :status, :checking_source
132
+ raise JobPrerequisitesNotMet unless @source
133
+ raise JobPrerequisitesNotMet unless @source
134
+ target_path = File.dirname @target.path
135
+ target_name = File.basename @target.path
136
+
137
+ # Read source file size
138
+ source_size = File.size @source
139
+ set :file_size, source_size
140
+
141
+ # Prepare FTP transfer
142
+ set :status, :checking_target
143
+ ftp = Net::FTP.new(@target.host)
144
+ ftp.passive = true
145
+ ftp.login
146
+ ftp.chdir(target_path)
147
+
148
+ # Check for target file presence
149
+ results = ftp.list(target_name)
150
+ #info "ftp.list: #{results}"
151
+ unless results.count.zero?
152
+ ftp.close
153
+ raise JobTargetPermission
154
+ end
155
+
156
+ # Do transfer
157
+ set :status, :uploading
158
+ ftp.putbinaryfile(@source, target_name, TRANSFER_CHUNK_SIZE) do |block|
159
+ # Update counters
160
+ transferred += block.bytesize
161
+
162
+ # Update job info
163
+ percent = (100.0 * transferred / source_size).round(1)
164
+ set :file_progress, percent
165
+ set :file_sent, transferred
166
+ end
167
+
168
+ # Close FTP connexion
169
+ ftp.close
170
+ end
171
+
172
+ end
173
+ end
@@ -0,0 +1,237 @@
1
+ module RestFtpDaemon
2
+
3
+ class API < Grape::API
4
+ version 'v1', using: :header, vendor: 'ftven'
5
+ format :json
6
+
7
+
8
+ ######################################################################
9
+ ####### INIT
10
+ ######################################################################
11
+ def initialize
12
+ # Setup logger
13
+ @@logger = Logger.new(APP_LOGTO, 'daily')
14
+ # @@queue = Queue.new
15
+
16
+ # Create new thread group
17
+ @@threads = ThreadGroup.new
18
+
19
+ # Other stuff
20
+ @@last_worker_id = 0
21
+ super
22
+ end
23
+
24
+ ######################################################################
25
+ ####### HELPERS
26
+ ######################################################################
27
+ helpers do
28
+ def api_error exception
29
+ {
30
+ :error => exception.class,
31
+ :errmsg => exception.message,
32
+ :backtrace => exception.backtrace.first,
33
+ #:backtrace => exception.backtrace,
34
+ }
35
+ end
36
+
37
+ def info msg=""
38
+ @@logger.info msg
39
+ end
40
+
41
+ def threads_with_id job_id
42
+ @@threads.list.select do |thread|
43
+ next unless thread[:job].is_a? Job
44
+ thread[:job].id == job_id
45
+ end
46
+ end
47
+
48
+ def job_describe job_id
49
+ # Find threads with tihs id
50
+ threads = threads_with_id job_id
51
+ raise RestFtpDaemon::JobNotFound if threads.empty?
52
+
53
+ # Find first job with tihs id
54
+ job = threads.first[:job]
55
+ raise RestFtpDaemon::JobNotFound unless job.is_a? Job
56
+ description = job.describe
57
+
58
+ # Return job description
59
+ description
60
+ end
61
+
62
+ def job_delete job_id
63
+ # Find threads with tihs id
64
+ threads = threads_with_id job_id
65
+ raise RestFtpDaemon::JobNotFound if threads.empty?
66
+
67
+ # Get description just before terminating the job
68
+ job = threads.first[:job]
69
+ raise RestFtpDaemon::JobNotFound unless job.is_a? Job
70
+ description = job.describe
71
+
72
+ # Kill those threads
73
+ threads.each do |t|
74
+ Thread.kill(t)
75
+ end
76
+
77
+ # Return job description
78
+ description
79
+ end
80
+
81
+ def job_list
82
+ @@threads.list.map do |thread|
83
+ next unless thread[:job].is_a? Job
84
+ thread[:job].describe
85
+ end
86
+ end
87
+
88
+ end
89
+
90
+
91
+ ######################################################################
92
+ ####### API DEFINITION
93
+ ######################################################################
94
+
95
+ # Spawn a new thread for this new job
96
+ # post '/push' do
97
+ # @@queue << rand(999)
98
+ # end
99
+
100
+ # Server global status
101
+ get '/' do
102
+ info "GET /"
103
+
104
+ status 200
105
+ {
106
+ app_name: APP_NAME,
107
+ hostname: `hostname`.chomp,
108
+ version: RestFtpDaemon::VERSION,
109
+ started: APP_STARTED,
110
+ uptime: (Time.now - APP_STARTED).round(1),
111
+ }
112
+ end
113
+
114
+ # Server test
115
+ get '/test' do
116
+ info "GET /tests"
117
+ begin
118
+ raise RestFtpDaemon::DummyException
119
+ rescue RestFtpDaemon::RestFtpDaemonException => exception
120
+ status 501
121
+ api_error exception
122
+ rescue Exception => exception
123
+ status 501
124
+ api_error exception
125
+ else
126
+ status 200
127
+ {}
128
+ end
129
+ end
130
+
131
+ # List jobs
132
+ get "/jobs" do
133
+ info "GET /jobs"
134
+ begin
135
+ response = job_list
136
+ rescue RestFtpDaemonException => exception
137
+ status 501
138
+ api_error exception
139
+ rescue Exception => exception
140
+ status 501
141
+ api_error exception
142
+ else
143
+ status 200
144
+ response
145
+ end
146
+ end
147
+
148
+ # Get job info
149
+ get "/jobs/:id" do
150
+ info "GET /jobs/#{params[:id]}"
151
+ begin
152
+ response = job_describe params[:id].to_i
153
+ rescue RestFtpDaemon::JobNotFound => exception
154
+ status 404
155
+ api_error exception
156
+ rescue RestFtpDaemonException => exception
157
+ status 500
158
+ api_error exception
159
+ rescue Exception => exception
160
+ status 501
161
+ api_error exception
162
+ else
163
+ status 200
164
+ response
165
+ end
166
+ end
167
+
168
+ # Delete jobs
169
+ delete "/jobs/:id" do
170
+ info "DELETE /jobs/#{params[:name]}"
171
+ begin
172
+ response = job_delete params[:id].to_i
173
+ rescue RestFtpDaemon::JobNotFound => exception
174
+ status 404
175
+ api_error exception
176
+ rescue RestFtpDaemonException => exception
177
+ status 500
178
+ api_error exception
179
+ rescue Exception => exception
180
+ status 501
181
+ api_error exception
182
+ else
183
+ status 200
184
+ response
185
+ end
186
+ end
187
+
188
+ # Spawn a new thread for this new job
189
+ post '/jobs' do
190
+ info "POST /jobs: #{request.body.read}"
191
+ begin
192
+ # Extract params
193
+ request.body.rewind
194
+ params = JSON.parse request.body.read
195
+
196
+ # Create a new job
197
+ job_id = @@last_worker_id += 1
198
+ job = Job.new(job_id, params)
199
+
200
+ # Put it inside a thread
201
+ th = Thread.new(job) do |thread|
202
+ # Tnitialize thread
203
+ Thread.abort_on_exception = true
204
+ Thread.current[:job] = job
205
+
206
+ # Do the job
207
+ job.process
208
+
209
+ # Wait for a few seconds before cleaning up the job
210
+ job.wander RestFtpDaemon::THREAD_SLEEP_BEFORE_DIE
211
+ end
212
+
213
+ # Stack it to the pool
214
+ #@@queue << job
215
+ @@threads.add th
216
+
217
+ # And start it asynchronously
218
+ #job.future.process
219
+
220
+ rescue JSON::ParserError => exception
221
+ status 406
222
+ api_error exception
223
+ rescue RestFtpDaemonException => exception
224
+ status 412
225
+ api_error exception
226
+ rescue Exception => exception
227
+ status 501
228
+ api_error exception
229
+ else
230
+ status 201
231
+ job.describe
232
+ end
233
+ end
234
+
235
+ end
236
+
237
+ end
@@ -1,12 +1,11 @@
1
1
  # coding: utf-8
2
2
  lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'version.rb'
4
+ require 'rest-ftp-daemon/config'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "rest-ftp-daemon"
8
- spec.version = "0.20.0"
9
- spec.date = "2014-08-14"
8
+ spec.date = Time.now.strftime("%Y-%m-%d")
10
9
  spec.authors = ["Bruno MEDICI"]
11
10
  spec.email = "rest-ftp-daemon@bmconseil.com"
12
11
  spec.description = "This is a pretty simple FTP client daemon, controlled through a RESTfull API"
@@ -15,38 +14,17 @@ Gem::Specification.new do |spec|
15
14
  spec.licenses = ["MIT"]
16
15
 
17
16
  spec.files = `git ls-files -z`.split("\x0")
18
- #spec.executables = ["rest-ftp-daemon"]
19
- #spec.executables = `git ls-files -- bin/*`.split('\n').map{ |f| File.basename(f) }
20
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
18
  spec.require_paths = ["lib"]
19
+ spec.version = RestFtpDaemon::VERSION
22
20
 
23
-
24
- spec.required_ruby_version = '>= 1.9'
21
+ spec.required_ruby_version = '>= 1.9.3'
25
22
 
26
23
  spec.add_development_dependency "bundler", "~> 1.6"
27
24
  spec.add_development_dependency "rake"
28
25
 
29
-
30
- # spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
31
- # spec.files = [
32
- # "Gemfile",
33
- # "Gemfile.lock",
34
- # "LICENSE.txt",
35
- # "README.md",
36
- # "Rakefile",
37
- # "VERSION",
38
- # "bin/rest-ftp-daemon",
39
- # "lib/config.rb",
40
- # "lib/config.ru",
41
- # "lib/errors.rb",
42
- # "lib/extend_threads.rb",
43
- # "lib/rest-ftp-daemon.rb",
44
- # "rest-ftp-daemon.gemspec",
45
- # "test/helper.rb",
46
- # "test/test_rest-ftp-daemon.rb"
47
- # ]
48
- # spec.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
49
- # spec.rubygems_version = "2.4.1"
26
+ spec.add_runtime_dependency "thin"
27
+ spec.add_runtime_dependency "grape"
28
+ spec.add_runtime_dependency "json"
50
29
 
51
30
  end
52
-
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rest-ftp-daemon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.30.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bruno MEDICI
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-08-14 00:00:00.000000000 Z
11
+ date: 2014-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -38,6 +38,48 @@ dependencies:
38
38
  - - ! '>='
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: thin
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: grape
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: json
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
41
83
  description: This is a pretty simple FTP client daemon, controlled through a RESTfull
42
84
  API
43
85
  email: rest-ftp-daemon@bmconseil.com
@@ -52,14 +94,13 @@ files:
52
94
  - LICENSE.txt
53
95
  - README.md
54
96
  - Rakefile
55
- - VERSION
56
97
  - bin/rest-ftp-daemon
57
- - lib/config.rb
58
- - lib/config.ru
59
- - lib/errors.rb
60
- - lib/extend_threads.rb
98
+ - config.ru
61
99
  - lib/rest-ftp-daemon.rb
62
- - lib/version.rb
100
+ - lib/rest-ftp-daemon/config.rb
101
+ - lib/rest-ftp-daemon/exceptions.rb
102
+ - lib/rest-ftp-daemon/job.rb
103
+ - lib/rest-ftp-daemon/server.rb
63
104
  - rest-ftp-daemon.gemspec
64
105
  - test/helper.rb
65
106
  - test/test_rest-ftp-daemon.rb
@@ -75,7 +116,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
75
116
  requirements:
76
117
  - - ! '>='
77
118
  - !ruby/object:Gem::Version
78
- version: '1.9'
119
+ version: 1.9.3
79
120
  required_rubygems_version: !ruby/object:Gem::Requirement
80
121
  requirements:
81
122
  - - ! '>='
data/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.9.0
data/lib/config.rb DELETED
@@ -1,3 +0,0 @@
1
- # Transfer configuration
2
- TRANSFER_CHUNK_SIZE = 100000
3
- THREAD_SLEEP_BEFORE_DIE = 120
data/lib/config.ru DELETED
@@ -1,12 +0,0 @@
1
- # Main libs
2
- require 'sinatra'
3
- require 'sinatra/base'
4
- require 'net/ftp'
5
- require 'json'
6
-
7
- # My local libs
8
- Dir[APP_ROOT+"/lib/*.rb"].each {|file| require File.expand_path file }
9
- #Dir[APP_ROOT+"/lib/*/*.rb"].each {|file| require File.expand_path file }
10
-
11
- # Start application
12
- run RestFtpDaemon
data/lib/errors.rb DELETED
@@ -1,12 +0,0 @@
1
- ERR_OK = 0
2
- ERR_BUSY = -1
3
-
4
- ERR_REQ_SOURCE_MISSING = 11
5
- ERR_REQ_TARGET_MISSING = 12
6
- ERR_REQ_TARGET_SCHEME = 13
7
-
8
- ERR_JOB_SOURCE_NOTFOUND = 21
9
- ERR_JOB_TARGET_UNPARSEABLE = 22
10
- ERR_JOB_PERMISSION = 24
11
- ERR_JOB_TARGET_PRESENT = 25
12
-
@@ -1,14 +0,0 @@
1
- class Thread
2
-
3
- def job
4
- # Basic fields
5
- job = self[:job]
6
-
7
- # Calculated fields
8
- job[:id] = self[:id]
9
- job[:runtime] = (Time.now - job[:created]).round(1)
10
-
11
- return job
12
- end
13
-
14
- end
data/lib/version.rb DELETED
@@ -1,3 +0,0 @@
1
- # module RestFtpDaemon
2
- VERSION = "0.20"
3
- # end