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 CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- YThkNmNkZTEwNzM2YzcwM2I2N2NiOWE5OTQwNDI1N2M0NTFjZWZhOA==
4
+ MDQwNWFlNDE3ZGUwZGY2ZTc1N2FiMGE3OWNkODMxN2VhODdkNDNiMA==
5
5
  data.tar.gz: !binary |-
6
- ZTllMzMyNGMzNTE1M2U5ZTBjZTc3MWJmYWNiYWJlNzQwM2NiNmMxNQ==
6
+ MmM2YmRkMDkwMjM2OTM2YjZlOTcyNjU0NmNmODQwNmJhNjI0YzdlNQ==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- ODQzMzc5MzRiZDZjZDAyM2QxZWQyMThjNTUyZmNlZWJhOGFiNTJiODAyYWJi
10
- YjExZmZiNGMzMDM5ZDRjMmRjYTMxMTNhZGU0ZWNkODMxMzVjZTQ3YWY2MTk5
11
- MjYxM2MwNDU5YzM2OWU1MGVjZjc5NTgwYWVjOTdmZTEzNjQ5MGM=
9
+ YzEzZjdmYzEyNWJhZTRkODc0NmY4NjRjNTMxNmZiZWFmMWIwOTg5YmM3YzE5
10
+ NjIwYTNlMTgzNzhhYTljYTUzNTk2NTk3YzJmOTgzYmNjNzAwY2NiMjFlMDYz
11
+ MjU3ZmZmNjBhYTA0ZWUzOWJkY2MyNWI1ZmYyM2M5ZDVmYWNiNTA=
12
12
  data.tar.gz: !binary |-
13
- NTBhZWM0NDM2ODNiY2ZjNTA0YjYxYWE3MTBlYWUwZmM3NjgxOGZhNmNlZDlk
14
- ZGEwOWE1ZDM4ZjU1N2FjYTQ2ZTNlMjllOGE5ODlhMTJjNjA5ZGFhYjFlODdj
15
- ZDBjM2Y1ZTVmODdiYTk0YjdkMGViMGE1ZDA4ZjQ0NzdhZWUxODE=
13
+ ZTEyZmVkYjIwYzQ2NjVkZjEyNTUzOGFjY2NkNGJjMzkyZWIzZDQ4ZWJiOGNi
14
+ OGUxMGY5Nzk4YzUxZWEzMjE5Mjg3NDhkMDI1MzFjMWYyNjYxMDk2NmYzZDY2
15
+ ODQ0NzIwMWY1NGQwYWZjYTE1NjRiMjFhNGZiYzI1Y2U2YjEwMjI=
data/Gemfile.lock CHANGED
@@ -1,9 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rest-ftp-daemon (0.22)
4
+ rest-ftp-daemon (0.30.1)
5
5
  grape
6
6
  json
7
+ thin (~> 1.6)
7
8
 
8
9
  GEM
9
10
  remote: http://rubygems.org/
@@ -21,10 +22,12 @@ GEM
21
22
  builder (3.2.2)
22
23
  coercible (1.0.0)
23
24
  descendants_tracker (~> 0.0.1)
25
+ daemons (1.1.9)
24
26
  descendants_tracker (0.0.4)
25
27
  thread_safe (~> 0.3, >= 0.3.1)
26
28
  equalizer (0.0.9)
27
- grape (0.8.0)
29
+ eventmachine (1.0.3)
30
+ grape (0.9.0)
28
31
  activesupport
29
32
  builder
30
33
  hashie (>= 2.1.0)
@@ -34,11 +37,11 @@ GEM
34
37
  rack-accept
35
38
  rack-mount
36
39
  virtus (>= 1.0.0)
37
- hashie (3.2.0)
40
+ hashie (3.3.1)
38
41
  i18n (0.6.11)
39
42
  ice_nine (0.11.0)
40
43
  json (1.8.1)
41
- minitest (5.4.0)
44
+ minitest (5.4.1)
42
45
  multi_json (1.10.1)
43
46
  multi_xml (0.5.5)
44
47
  rack (1.5.2)
@@ -47,6 +50,10 @@ GEM
47
50
  rack-mount (0.8.3)
48
51
  rack (>= 1.0.0)
49
52
  rake (10.3.2)
53
+ thin (1.6.2)
54
+ daemons (>= 1.0.9)
55
+ eventmachine (>= 1.0.0)
56
+ rack (>= 1.0.0)
50
57
  thread_safe (0.3.4)
51
58
  tzinfo (1.2.2)
52
59
  thread_safe (~> 0.1)
data/README.md CHANGED
@@ -5,10 +5,32 @@ This is a pretty simple FTP client daemon, controlled through a RESTfull API.
5
5
 
6
6
  As of today, its main features are :
7
7
 
8
- * Delegate a transfer job, ```PUT```'ing posting simple JSON structure
8
+ * Delegate a transfer job by ``POST```'ing a simple JSON structure
9
9
  * Spawn a dedicated thread to handle this job in its own context
10
- * Report transfer status, progress and errors for each delegated job
10
+ * Report transfer status, progress and errors for each job in realtime
11
11
  * Expose JSON status of workers on ```GET /jobs/``` for automated monitoring
12
+ * Parralelize jobs as soon as they arrive
13
+
14
+ Expected features in a short-time range :
15
+
16
+ * Handle job queues
17
+ * Handle job priorities
18
+ * Allow change of priorities or other attributes after a job has been started
19
+ * Provide RESTful notifications to the requesting client
20
+ * Offer a basic dashboard directly within the daemon HTTP interface
21
+ * Periodically send an update-notification with transfer status and progress
22
+ * Allow fallback file source when first file path is unavailable (failover)
23
+ * Some refactoring may be needed after thos steps
24
+ * Provide swagger-style API documentation
25
+ * Authenticate API clients
26
+
27
+
28
+
29
+ Documentation TODO
30
+ ------------------------------------------------------------------------------------
31
+ overwrite: any non empty value allows overwriting
32
+ todo: queues
33
+
12
34
 
13
35
 
14
36
  Installation
@@ -19,7 +41,7 @@ This project is available as a rubygem, requires on ruby >= 1.9.3 and rubygems i
19
41
  Get and install the gem from rubygems.org:
20
42
 
21
43
  ```
22
- # apt-get install ruby2.1 rubygems
44
+ # apt-get install ruby1.9.3 ruby-dev rubygems gcc g++
23
45
  gem install rest-ftp-daemon --no-ri --no-rdoc
24
46
  ```
25
47
 
@@ -41,6 +63,8 @@ For now, daemon logs to ```APP_LOGTO``` defined in ```lib/config.rb```
41
63
  Usage examples
42
64
  ------------------------------------------------------------------------------------
43
65
 
66
+ Requesting notifications is achieved by passing a "notify" key in the request, with a callback URL. This URL will be called at some points, ``POST```'ing a generic JSON structure with progress information.
67
+
44
68
  Start a job to transfer a file named "file.iso" to a local FTP server
45
69
 
46
70
  ```
@@ -48,17 +72,17 @@ curl -H "Content-Type: application/json" -X POST -D /dev/stdout -d \
48
72
  '{"source":"~/file.iso","target":"ftp://anonymous@localhost/incoming/dest2.iso"}' "http://localhost:3000/jobs"
49
73
  ```
50
74
 
51
- Start a job to transfer a file named "file.dmg" to a local FTP server
75
+ Start a job to transfer a file, and request notifications ``POST```'ed on "http://requestb.in/1321axg1"
52
76
 
53
77
  ```
54
78
  curl -H "Content-Type: application/json" -X POST -D /dev/stdout -d \
55
- '{"source":"~/file.dmg","target":"ftp://anonymous@localhost/incoming/dest4.dmg"}' "http://localhost:3000/jobs"
79
+ '{"source":"~/file.dmg","target":"ftp://anonymous@localhost/incoming/dest4.dmg","notify":"http://requestb.in/1321axg1"}' "http://localhost:3000/jobs"
56
80
  ```
57
81
 
58
82
  Get status of a specific job based on its name
59
83
 
60
84
  ```
61
- curl -H "Content-Type: application/json" -X DELETE -D /dev/stdout "http://localhost:3000/jobs/bob-45320-1"
85
+ curl -H "Content-Type: application/json" -X GET -D /dev/stdout "http://localhost:3000/jobs/bob-45320-1"
62
86
  ```
63
87
 
64
88
  Delete a specific job based on its name
data/bin/rest-ftp-daemon CHANGED
@@ -1,23 +1,30 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  # Libs and init
4
- require 'thin'
4
+ require "thin"
5
5
 
6
6
  # Initialize some local constants
7
7
  APP_ROOT = File.dirname(__FILE__) + '/../'
8
- APP_NAME = 'rest-ftp-daemon'
8
+ rackup_file = File.expand_path "#{APP_ROOT}/config.ru"
9
9
 
10
- APP_STARTED = Time.now
11
- APP_DEFAULT_PORT = 3000
12
- APP_LOGTO = "/tmp/#{APP_NAME}.log"
10
+ # Include config file
11
+ require File.expand_path "#{APP_ROOT}/lib/rest-ftp-daemon/config.rb"
13
12
 
14
13
  # Prepare thin
15
- rackup_file = File.expand_path "#{APP_ROOT}/config.ru"
16
14
  argv = ARGV
17
15
  argv << ["-R", rackup_file] unless ARGV.include?("-R")
18
- argv << ["-p", APP_DEFAULT_PORT.to_s] unless ARGV.include?("-p")
16
+ argv << ["-l", "/tmp/thin.log"] unless ARGV.include?("-l")
17
+ argv << ["--daemonize"] unless ARGV.include?("--daemonize")
19
18
  argv << ["-e", "production"] unless ARGV.include?("-e")
20
- argv << ["--daemonize"] unless ARGV.include?("-R")
19
+ argv << ["-p", (RestFtpDaemon::PORT).to_s] unless ARGV.include?("-p")
21
20
  #argv << ["--stats", "/stats"] unless ARGV.include?("--stats")
22
21
 
23
- Thin::Runner.new(argv.flatten).run!
22
+ # Start Thin with this rackup configuration
23
+ puts "#{RestFtpDaemon::NAME}: using [#{argv.join (' ')}]"
24
+ begin
25
+ Thin::Runner.new(argv.flatten).run!
26
+ rescue Thin::PidFileNotFound
27
+ puts "#{RestFtpDaemon::NAME}: not running (Thin::PidFileNotFound)"
28
+ end
29
+
30
+
data/config.ru CHANGED
@@ -2,5 +2,13 @@
2
2
  $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), 'lib'))
3
3
  require 'rest-ftp-daemon'
4
4
 
5
+ # Some extra constants
6
+ APP_STARTED = Time.now
7
+
8
+ # Create worker pool
9
+ $queue = RestFtpDaemon::JobQueue.new
10
+ $pool = RestFtpDaemon::WorkerPool.new(1)
11
+
5
12
  # Start REST FTP Daemon
6
- run RestFtpDaemon::API
13
+ #run Rack::Cascade.new [Rack::File.new("/public"), RestFtpDaemon::API::Root]
14
+ run Rack::Cascade.new [RestFtpDaemon::API::Root]
@@ -2,9 +2,21 @@
2
2
  require 'json'
3
3
  require 'grape'
4
4
  require 'net/ftp'
5
+ require 'net/http'
6
+ require 'securerandom'
7
+ # require 'celluloid/autostart'
5
8
 
6
9
  # My libs
7
10
  require 'rest-ftp-daemon/config'
8
11
  require 'rest-ftp-daemon/exceptions'
12
+ require 'rest-ftp-daemon/common'
13
+ require 'rest-ftp-daemon/job_queue'
14
+ require 'rest-ftp-daemon/worker_pool'
15
+ require 'rest-ftp-daemon/logger'
9
16
  require 'rest-ftp-daemon/job'
10
- require 'rest-ftp-daemon/server'
17
+ require 'rest-ftp-daemon/notification'
18
+ require 'rest-ftp-daemon/api/defaults'
19
+ require 'rest-ftp-daemon/api/jobs'
20
+ require 'rest-ftp-daemon/api/root'
21
+ require 'rest-ftp-daemon/www'
22
+
@@ -0,0 +1,58 @@
1
+ module RestFtpDaemon
2
+ module API
3
+ module Defaults
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ #version 'v1', using: :header, vendor: 'ftven'
8
+ format :json
9
+ do_not_route_head!
10
+ do_not_route_options!
11
+
12
+ # before do
13
+ # header['Access-Control-Allow-Origin'] = '*'
14
+ # header['Access-Control-Request-Method'] = '*'
15
+ # end
16
+
17
+ # Handle authentication
18
+ # http_basic do |username, password|
19
+ # User.authenticate!(username, password)
20
+ # end
21
+
22
+ # # global handler for simple not found case
23
+ # rescue_from ActiveRecord::RecordNotFound do |e|
24
+ # error_response(message: e.message, status: 404)
25
+ # end
26
+
27
+ # # global exception handler, used for error notifications
28
+ # rescue_from :all do |e|
29
+ # if Rails.env.development?
30
+ # raise e
31
+ # else
32
+ # Raven.capture_exception(e)
33
+ # error_response(message: "Internal server error", status: 500)
34
+ # end
35
+ # end
36
+
37
+ # # HTTP header based authentication
38
+ # before do
39
+ # error!('Unauthorized', 401) unless headers['Authorization'] == "some token"
40
+ # end
41
+
42
+ helpers do
43
+ def api_error exception
44
+ {
45
+ :error => exception.class,
46
+ :errmsg => exception.message,
47
+ :backtrace => exception.backtrace.first,
48
+ #:backtrace => exception.backtrace,
49
+ }
50
+ end
51
+ end
52
+
53
+ end
54
+
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,151 @@
1
+ module RestFtpDaemon
2
+ module API
3
+
4
+ class Jobs < Grape::API
5
+ include RestFtpDaemon::API::Defaults
6
+ logger ActiveSupport::Logger.new APP_LOGTO, 'daily'
7
+
8
+ params do
9
+ optional :overwrite, type: Integer, default: false
10
+ end
11
+
12
+ helpers do
13
+
14
+ def threads_with_id job_id
15
+ $threads.list.select do |thread|
16
+ next unless thread[:job].is_a? Job
17
+ thread[:job].id == job_id
18
+ end
19
+ end
20
+
21
+ def job_describe job_id
22
+ raise RestFtpDaemon::JobNotFound if ($queue.queued_size==0 && $queue.popped_size==0)
23
+
24
+ # Find job with this id
25
+ found = $queue.all.select { |job| job.id == job_id }.first
26
+ raise RestFtpDaemon::JobNotFound if found.nil?
27
+ raise RestFtpDaemon::JobNotFound unless found.is_a? Job
28
+
29
+ # Return job description
30
+ found.describe
31
+ end
32
+
33
+ # def job_delete job_id
34
+ # end
35
+
36
+ def job_list
37
+ $queue.all.map do |item|
38
+ next unless item.is_a? Job
39
+ item.describe
40
+ end
41
+ end
42
+
43
+ end
44
+
45
+ desc "Get information about a specific job"
46
+ params do
47
+ requires :id, type: Integer, desc: "job id"
48
+ end
49
+ get ':id' do
50
+ info "GET /jobs/#{params[:id]}"
51
+ begin
52
+ response = job_describe params[:id].to_i
53
+ rescue RestFtpDaemon::JobNotFound => exception
54
+ status 404
55
+ api_error exception
56
+ rescue RestFtpDaemonException => exception
57
+ status 500
58
+ api_error exception
59
+ rescue Exception => exception
60
+ status 501
61
+ api_error exception
62
+ else
63
+ status 200
64
+ response
65
+ end
66
+ end
67
+
68
+ # Delete jobs
69
+ desc "Kill and remove a specific job"
70
+ delete ':id' do
71
+ info "DELETE /jobs/#{params[:name]}"
72
+ status 501
73
+ # begin
74
+ # response = job_delete params[:id].to_i
75
+ # rescue RestFtpDaemon::JobNotFound => exception
76
+ # status 404
77
+ # api_error exception
78
+ # rescue RestFtpDaemonException => exception
79
+ # status 500
80
+ # api_error exception
81
+ # rescue Exception => exception
82
+ # status 501
83
+ # api_error exception
84
+ # else
85
+ # status 200
86
+ # response
87
+ # end
88
+ end
89
+
90
+ # List jobs
91
+ desc "Get a list of jobs"
92
+ get do
93
+ info "GET /jobs"
94
+ begin
95
+ response = job_list
96
+ rescue RestFtpDaemonException => exception
97
+ status 501
98
+ api_error exception
99
+ rescue Exception => exception
100
+ status 501
101
+ api_error exception
102
+ else
103
+ status 200
104
+ response
105
+ end
106
+ end
107
+
108
+
109
+ # Spawn a new thread for this new job
110
+ desc "Create a new job"
111
+ post do
112
+ info "POST /jobs: #{request.body.read}"
113
+ begin
114
+ # Extract params
115
+ request.body.rewind
116
+ params = JSON.parse request.body.read
117
+
118
+ # Create a new job
119
+ job_id = $last_worker_id += 1
120
+ job = Job.new(job_id, params)
121
+
122
+ # And psuh it to the queue
123
+ $queue.push job
124
+
125
+ # Later: start it asynchronously
126
+ #job.future.process
127
+
128
+ rescue JSON::ParserError => exception
129
+ status 406
130
+ api_error exception
131
+ rescue RestFtpDaemonException => exception
132
+ status 412
133
+ api_error exception
134
+ rescue Exception => exception
135
+ status 501
136
+ api_error exception
137
+ else
138
+ status 201
139
+ job.describe
140
+ end
141
+ end
142
+
143
+ protected
144
+
145
+ def progname
146
+ "API::Jobs"
147
+ end
148
+
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,79 @@
1
+ module RestFtpDaemon
2
+ module API
3
+
4
+ class Root < Grape::API
5
+ include RestFtpDaemon::API::Defaults
6
+ logger ActiveSupport::Logger.new APP_LOGTO, 'daily'
7
+ #add_swagger_documentation
8
+
9
+ mount RestFtpDaemon::API::Jobs => '/jobs'
10
+ # mount RestFtpDaemon::API::Workers => '/workers'
11
+
12
+ helpers do
13
+ def info message, level = 0
14
+ Root.logger.add(Logger::INFO, "#{' '*level} #{message}", "API::Root")
15
+ end
16
+
17
+ def job_list_by_status
18
+ statuses = {}
19
+ alljobs = $queue.all.map do |item|
20
+ next unless item.is_a? Job
21
+ statuses[item.get_status] ||= 0
22
+ statuses[item.get_status] +=1
23
+ end
24
+
25
+ statuses
26
+ end
27
+ end
28
+
29
+ ######################################################################
30
+ ####### INIT
31
+ ######################################################################
32
+ def initialize
33
+ $last_worker_id = 0
34
+ super
35
+ end
36
+
37
+
38
+ ######################################################################
39
+ ####### API DEFINITION
40
+ ######################################################################
41
+
42
+ # Server global status
43
+ get '/' do
44
+ info "GET /"
45
+ status 200
46
+ return {
47
+ name: RestFtpDaemon::NAME,
48
+ hostname: `hostname`.chomp,
49
+ version: RestFtpDaemon::VERSION,
50
+ started: APP_STARTED,
51
+ uptime: (Time.now - APP_STARTED).round(1),
52
+ status: job_list_by_status,
53
+ queue_size: $queue.all_size,
54
+ jobs_queued: $queue.queued.collect(&:id),
55
+ jobs_popped: $queue.popped.collect(&:id),
56
+ routes: RestFtpDaemon::API::Root::routes,
57
+ }
58
+ end
59
+
60
+ # Server test
61
+ get '/debug' do
62
+ info "GET /debug/"
63
+ begin
64
+ raise RestFtpDaemon::DummyException
65
+ rescue RestFtpDaemon::RestFtpDaemonException => exception
66
+ status 501
67
+ api_error exception
68
+ rescue Exception => exception
69
+ status 501
70
+ api_error exception
71
+ else
72
+ status 200
73
+ {}
74
+ end
75
+ end
76
+
77
+ end
78
+ end
79
+ end