rest-ftp-daemon 0.30.1 → 0.41

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
- 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