rest-ftp-daemon 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "sinatra"
4
+ gem "json"
5
+
6
+ group :development do
7
+ gem "shoulda", ">= 0"
8
+ gem "rdoc", "~> 3.12"
9
+ gem "bundler", "~> 1.0"
10
+ gem "jeweler", "~> 2.0.1"
11
+ gem "simplecov", ">= 0"
12
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,89 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activesupport (4.1.4)
5
+ i18n (~> 0.6, >= 0.6.9)
6
+ json (~> 1.7, >= 1.7.7)
7
+ minitest (~> 5.1)
8
+ thread_safe (~> 0.1)
9
+ tzinfo (~> 1.1)
10
+ addressable (2.3.6)
11
+ builder (3.2.2)
12
+ descendants_tracker (0.0.4)
13
+ thread_safe (~> 0.3, >= 0.3.1)
14
+ docile (1.1.5)
15
+ faraday (0.9.0)
16
+ multipart-post (>= 1.2, < 3)
17
+ git (1.2.7)
18
+ github_api (0.12.0)
19
+ addressable (~> 2.3)
20
+ descendants_tracker (~> 0.0.4)
21
+ faraday (~> 0.8, < 0.10)
22
+ hashie (>= 3.2)
23
+ multi_json (>= 1.7.5, < 2.0)
24
+ nokogiri (~> 1.6.3)
25
+ oauth2
26
+ hashie (3.2.0)
27
+ highline (1.6.21)
28
+ i18n (0.6.11)
29
+ jeweler (2.0.1)
30
+ builder
31
+ bundler (>= 1.0)
32
+ git (>= 1.2.5)
33
+ github_api
34
+ highline (>= 1.6.15)
35
+ nokogiri (>= 1.5.10)
36
+ rake
37
+ rdoc
38
+ json (1.8.1)
39
+ jwt (1.0.0)
40
+ mini_portile (0.6.0)
41
+ minitest (5.4.0)
42
+ multi_json (1.10.1)
43
+ multi_xml (0.5.5)
44
+ multipart-post (2.0.0)
45
+ nokogiri (1.6.3.1)
46
+ mini_portile (= 0.6.0)
47
+ oauth2 (1.0.0)
48
+ faraday (>= 0.8, < 0.10)
49
+ jwt (~> 1.0)
50
+ multi_json (~> 1.3)
51
+ multi_xml (~> 0.5)
52
+ rack (~> 1.2)
53
+ rack (1.5.2)
54
+ rack-protection (1.5.3)
55
+ rack
56
+ rake (10.3.2)
57
+ rdoc (3.12.2)
58
+ json (~> 1.4)
59
+ shoulda (3.5.0)
60
+ shoulda-context (~> 1.0, >= 1.0.1)
61
+ shoulda-matchers (>= 1.4.1, < 3.0)
62
+ shoulda-context (1.2.1)
63
+ shoulda-matchers (2.6.2)
64
+ activesupport (>= 3.0.0)
65
+ simplecov (0.9.0)
66
+ docile (~> 1.1.0)
67
+ multi_json
68
+ simplecov-html (~> 0.8.0)
69
+ simplecov-html (0.8.0)
70
+ sinatra (1.4.5)
71
+ rack (~> 1.4)
72
+ rack-protection (~> 1.4)
73
+ tilt (~> 1.3, >= 1.3.4)
74
+ thread_safe (0.3.4)
75
+ tilt (1.4.1)
76
+ tzinfo (1.2.1)
77
+ thread_safe (~> 0.1)
78
+
79
+ PLATFORMS
80
+ ruby
81
+
82
+ DEPENDENCIES
83
+ bundler (~> 1.0)
84
+ jeweler (~> 2.0.1)
85
+ json
86
+ rdoc (~> 3.12)
87
+ shoulda
88
+ simplecov
89
+ sinatra
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2014 Bruno Medici Consultant
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,88 @@
1
+ ## rest-ftp-daemon ##
2
+
3
+ This is a fairly basic FTP client daemon, driven by REST-based webservice calls.
4
+ As of today, its main features are :
5
+
6
+ * Start a job through a basic structure posted with PUT /jobs
7
+ * Spawn a dedicated thread to handle this job in its own contexte
8
+ * Report transfer progress, error and activity for each job
9
+ * Gather jobs status into the main process to get a global view
10
+ * Report JSON status of workers on GET /jobs/ for automated monitoring
11
+
12
+
13
+ ## Quick setup ##
14
+
15
+ This project requires ruby >= 1.9 and rubygems installed.
16
+
17
+ Furthermore, gems are required to run it:
18
+
19
+ ``` gem install sinatra ```
20
+
21
+ Starting the daemon:
22
+
23
+ ``` thin start ```
24
+
25
+
26
+ ## Basic usage ##
27
+
28
+ Starting a job transferring file named "file.ova"
29
+
30
+ curl -H "Content-Type: application/json" -X POST -D /dev/stdout -d \
31
+ '{"source":"~/file.ova","target":"ftp://anonymous@localhost/incoming/dest2.ova"}' "http://localhost:3000/jobs"
32
+
33
+
34
+ Starting a job transferring file named "dmg"
35
+
36
+ curl -H "Content-Type: application/json" -X POST -D /dev/stdout -d \
37
+ '{"source":"~/file.dmg","target":"ftp://anonymous@localhost/incoming/dest4.dmg"}' "http://localhost:3000/jobs"
38
+
39
+
40
+ Delete a specific job
41
+
42
+ curl -H "Content-Type: application/json" -X DELETE -D /dev/stdout "http://localhost:3000/jobs/bob-45320-1"
43
+
44
+
45
+
46
+ ## Getting status ##
47
+
48
+ GET /jobs
49
+
50
+ Would return:
51
+
52
+ [
53
+ {
54
+ "id": "bob-49126-8",
55
+ "process": "sleep",
56
+ "status": "transferring",
57
+ "context": {
58
+ "source": "~\/file.ova",
59
+ "target": "ftp:\/\/anonymous@localhost\/incoming\/dest2.ova",
60
+ "code": -1,
61
+ "errmsg": "running",
62
+ "source_size": 1849036800,
63
+ "progress": 1.9,
64
+ "transferred": 34800000
65
+ }
66
+ },
67
+ {
68
+ "id": "bob-49126-9",
69
+ "process": "sleep",
70
+ "status": "transferring",
71
+ "context": {
72
+ "source": "~\/file.dmg",
73
+ "target": "ftp:\/\/anonymous@localhost\/incoming\/dest4.dmg",
74
+ "code": -1,
75
+ "errmsg": "running",
76
+ "source_size": 37109074,
77
+ "progress": 32.9,
78
+ "transferred": 12200000
79
+ }
80
+ }
81
+ ]
82
+
83
+
84
+ ### About ###
85
+
86
+ Bruno MEDICI
87
+
88
+ http://bmconseil.com/
data/Rakefile ADDED
@@ -0,0 +1,51 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
17
+ gem.name = "rest-ftp-daemon"
18
+ gem.homepage = "http://github.com/bmedici/rest-ftp-daemon"
19
+ gem.license = "MIT"
20
+ gem.summary = "RESTful FTP client daemon"
21
+ gem.description = "A fairly basic FTP client daemon, driven by RESTful webservice calls"
22
+ gem.email = "rest-ftp-daemon@bmconseil.com"
23
+ gem.authors = ["Bruno"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+
35
+ desc "Code coverage detail"
36
+ task :simplecov do
37
+ ENV['COVERAGE'] = "true"
38
+ Rake::Task['test'].execute
39
+ end
40
+
41
+ task :default => :test
42
+
43
+ require 'rdoc/task'
44
+ Rake::RDocTask.new do |rdoc|
45
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
46
+
47
+ rdoc.rdoc_dir = 'rdoc'
48
+ rdoc.title = "rest-ftp-daemon #{version}"
49
+ rdoc.rdoc_files.include('README*')
50
+ rdoc.rdoc_files.include('lib/**/*.rb')
51
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Libs and init
4
+ require 'thin'
5
+ APP_ROOT = File.dirname(__FILE__) + '/../'
6
+ DEFAULT_PORT = 3000
7
+
8
+ # Prepare thin
9
+ rackup_file = File.expand_path "#{APP_ROOT}/lib/rest-ftp-daemon/config.ru"
10
+ argv = ARGV
11
+ argv << ["-R", rackup_file] unless ARGV.include?("-R")
12
+ argv << ["-p", DEFAULT_PORT.to_s] unless ARGV.include?("-p")
13
+ argv << ["-e", "production"] unless ARGV.include?("-e")
14
+
15
+ # Start thin
16
+ Thin::Runner.new(argv.flatten).run!
data/lib/config.rb ADDED
@@ -0,0 +1,2 @@
1
+ TRANSFER_CHUNK_SIZE = 100000
2
+ THREAD_SLEEP_BEFORE_DIE = 120
data/lib/errors.rb ADDED
@@ -0,0 +1,16 @@
1
+ # Error codes
2
+ # O: all ok
3
+ # 1x: request body errors
4
+ # 2x: transfer errors
5
+
6
+ ERR_OK = 0
7
+ ERR_BUSY = -1
8
+
9
+ ERR_REQ_SOURCE_MISSING = 11
10
+ ERR_REQ_TARGET_MISSING = 12
11
+ ERR_REQ_TARGET_SCHEME = 13
12
+
13
+ ERR_JOB_SOURCE_NOTFOUND = 21
14
+ ERR_JOB_TARGET_UNPARSEABLE = 22
15
+ ERR_JOB_PERMISSION = 24
16
+
@@ -0,0 +1,242 @@
1
+ class RestFtpDaemon < Sinatra::Base
2
+ helpers Sinatra::JSON
3
+
4
+ # General config
5
+ configure :development, :production do
6
+
7
+ # Create new thread group
8
+ @@threads = ThreadGroup.new
9
+ #set :dummy, true
10
+ # set :sessions, false
11
+ # set :logging, true
12
+ # set :root, APP_ROOT + '/lib/'
13
+ end
14
+
15
+ # Server initialization
16
+ def initialize
17
+ # Setup logger
18
+ @logger = Logger.new(APP_ROOT + '/main.log','daily')
19
+ @logger.level = Logger::INFO
20
+
21
+ # Other stuff
22
+ @@last_worker_id = 0
23
+ @@hostname = `hostname`.chomp
24
+
25
+ super
26
+ end
27
+
28
+ # Server global status
29
+ get "/" do
30
+ content_type :json
31
+ json get_status
32
+ end
33
+
34
+ # List jobs
35
+ get "/jobs" do
36
+ # Build response
37
+ content_type :json
38
+ json get_jobs
39
+ #@@threads.count
40
+ end
41
+
42
+ # List jobs
43
+ delete "/jobs/:name" do
44
+ # Kill this job
45
+ ret = delete_job params[:name]
46
+
47
+ # Fail if no process has been killed
48
+ error 404 if ret<1
49
+
50
+ # Build response
51
+ content_type :json
52
+ json nil
53
+ end
54
+
55
+ # Spawn a new thread for this new job
56
+ post '/jobs' do
57
+ request.body.rewind
58
+ payload = JSON.parse request.body.read
59
+ info "POST / with #{payload.to_json}"
60
+
61
+ # Spawn a thread
62
+ #config = {}
63
+ result = new_job payload
64
+
65
+ # Build response
66
+ content_type :json
67
+ json result
68
+ end
69
+
70
+ protected
71
+
72
+ def process_job
73
+ # Init
74
+ info "process_job: starting"
75
+ context = Thread.current[:context]
76
+ transferred = 0
77
+
78
+ # Check source
79
+ job_source = File.expand_path(context["source"])
80
+ if !(File.exists? job_source)
81
+ job_status ERR_JOB_SOURCE_NOTFOUND, :ERR_JOB_SOURCE_NOTFOUND
82
+ return
83
+ end
84
+ info "process_job: job_source: #{job_source}"
85
+ source_size = File.size job_source
86
+
87
+ # Check target
88
+ job_target = context["target"]
89
+ target = URI(job_target) rescue nil
90
+ if job_target.nil? || target.nil?
91
+ job_status ERR_JOB_TARGET_UNPARSEABLE, :ERR_JOB_TARGET_UNPARSEABLE
92
+ return
93
+ end
94
+ info "process_job: job_target: #{job_target}"
95
+
96
+ # Split URI
97
+ target_path = File.dirname target.path
98
+ target_name = File.basename target.path
99
+ info "ftp_transfer: job_target.host [#{target.host}]"
100
+ info "ftp_transfer: target_path [#{target_path}]"
101
+ info "ftp_transfer: target_name [#{target_name}]"
102
+
103
+ # Prepare FTP transfer
104
+ ftp = Net::FTP.new(target.host)
105
+ ftp.passive = true
106
+ ftp.login
107
+ ftp.chdir(target_path)
108
+
109
+ # Do transfer
110
+ info "source: starting stransfer"
111
+ Thread.current[:status] = :transferring
112
+ job_status ERR_BUSY, :running
113
+ job_set :source_size, source_size
114
+
115
+ begin
116
+ ftp.putbinaryfile(job_source, target_name, TRANSFER_CHUNK_SIZE) do |block|
117
+ # Update thread info
118
+ percent = (100.0 * transferred / source_size).round(1)
119
+ info "transferring [#{percent} %] of [#{target_name}]"
120
+ job_set :progress, percent
121
+ job_set :transferred, transferred
122
+
123
+ # Update counters
124
+ transferred += TRANSFER_CHUNK_SIZE
125
+ end
126
+
127
+ rescue Net::FTPPermError
128
+ Thread.current[:status] = :failed
129
+ job_status ERR_JOB_PERMISSION, :ERR_JOB_PERMISSION
130
+ info "source: FAILED: PERMISSIONS ERROR"
131
+
132
+ else
133
+ Thread.current[:status] = :finished
134
+ job_status ERR_OK, :finished
135
+ info "source: finished stransfer"
136
+ end
137
+
138
+ ftp.close
139
+ end
140
+
141
+ def get_status
142
+ {
143
+ greeting: "REST FTP daemon: up and running",
144
+ jobs_count: @@threads.list.count,
145
+ }
146
+ end
147
+
148
+ def get_jobs
149
+ output = []
150
+ @@threads.list.each do |thread|
151
+ output << {
152
+ :id => thread[:name],
153
+ :process => thread.status,
154
+ :status => thread[:status],
155
+ :context => thread[:context],
156
+ }
157
+ end
158
+ output
159
+ end
160
+
161
+
162
+ def delete_job name
163
+ count = 0
164
+ @@threads.list.collect do |thread|
165
+ next unless thread[:name] == name
166
+ Thread.kill(thread)
167
+ count += 1
168
+ end
169
+ count
170
+ end
171
+
172
+ def new_job context = {}
173
+ info "new_job: creating thread"
174
+
175
+ # Generate name
176
+ @@last_worker_id +=1
177
+ host = @@hostname.split('.')[0]
178
+ name = "#{host}-#{Process.pid.to_s}-#{@@last_worker_id}"
179
+ info "new_job: creating thread [#{name}]"
180
+
181
+ # Parse parameters
182
+ job_source = context["source"]
183
+ job_target = context["target"]
184
+ return { code: ERR_REQ_SOURCE_MISSING, errmsg: :ERR_REQ_SOURCE_MISSING} if job_source.nil?
185
+ return { code: ERR_REQ_TARGET_MISSING, errmsg: :ERR_REQ_TARGET_MISSING} if job_target.nil?
186
+ #return { code: ERR_TRX_SOURCE_FILE_NOT_FOUND, errmsg: "ERR_TRX_SOURCE_FILE_NOT_FOUND [#{job_source}]"} unless File.exists? job_source
187
+
188
+ # Parse dest URI
189
+ target = URI(job_target)
190
+ info target.scheme
191
+ return { code: ERR_REQ_TARGET_SCHEME, errmsg: :ERR_REQ_TARGET_SCHEME} unless target.scheme == "ftp"
192
+
193
+ # Create thread
194
+ job = Thread.new(name, job) do
195
+ # Initialize context
196
+ Thread.current[:name] = name
197
+ Thread.current[:created] = Time.now.to_f;
198
+ Thread.current[:status] = :thread_initializing
199
+
200
+ # Store job info
201
+ Thread.current[:context] = context
202
+ job_status ERR_BUSY, :thread_initializing
203
+ Thread.abort_on_exception = true
204
+
205
+ # Do the job
206
+ info "new_job: thread running"
207
+ process_job
208
+
209
+ # Sleep a few seconds before dying
210
+ Thread.current[:status] = :thread_ending
211
+ sleep THREAD_SLEEP_BEFORE_DIE
212
+ info "new_job: thread finished"
213
+ end
214
+
215
+ # Keep thread in thread group
216
+ info "new_job: attaching thread [#{name}] to group"
217
+ @@threads.add job
218
+
219
+ return { code: 0, errmsg: 'success', name: name, context: context }
220
+ end
221
+
222
+ def log level, msg=""
223
+ @logger.send(level.to_s, msg)
224
+ end
225
+
226
+ def info msg=""
227
+ log :info, msg
228
+ end
229
+
230
+ def job_status code, errmsg
231
+ Thread.current[:context] ||= {}
232
+ Thread.current[:context][:code] = code
233
+ Thread.current[:context][:errmsg] = errmsg
234
+ end
235
+
236
+ def job_set attribute, value
237
+ Thread.current[:context] ||= {}
238
+ Thread.current[:context][attribute] = value
239
+ end
240
+
241
+
242
+ end
@@ -0,0 +1,13 @@
1
+ # Main libs
2
+ require 'sinatra'
3
+ require 'sinatra/base'
4
+ require "sinatra/json"
5
+ require "sinatra/config_file"
6
+ require 'net/ftp'
7
+ require 'json'
8
+
9
+ # My local libs
10
+ Dir[APP_ROOT+"/lib/*.rb"].each {|file| require File.expand_path file }
11
+
12
+ # Start application
13
+ run RestFtpDaemon
data/test/helper.rb ADDED
@@ -0,0 +1,34 @@
1
+ require 'simplecov'
2
+
3
+ module SimpleCov::Configuration
4
+ def clean_filters
5
+ @filters = []
6
+ end
7
+ end
8
+
9
+ SimpleCov.configure do
10
+ clean_filters
11
+ load_adapter 'test_frameworks'
12
+ end
13
+
14
+ ENV["COVERAGE"] && SimpleCov.start do
15
+ add_filter "/.rvm/"
16
+ end
17
+ require 'rubygems'
18
+ require 'bundler'
19
+ begin
20
+ Bundler.setup(:default, :development)
21
+ rescue Bundler::BundlerError => e
22
+ $stderr.puts e.message
23
+ $stderr.puts "Run `bundle install` to install missing gems"
24
+ exit e.status_code
25
+ end
26
+ require 'test/unit'
27
+ require 'shoulda'
28
+
29
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
30
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
31
+ require 'rest-ftp-daemon'
32
+
33
+ class Test::Unit::TestCase
34
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestRestFtpDaemon < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,176 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rest-ftp-daemon
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Bruno
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-07-31 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: sinatra
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: json
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: shoulda
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rdoc
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '3.12'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: '3.12'
78
+ - !ruby/object:Gem::Dependency
79
+ name: bundler
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: '1.0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: '1.0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: jeweler
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: 2.0.1
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: 2.0.1
110
+ - !ruby/object:Gem::Dependency
111
+ name: simplecov
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: A fairly basic FTP client daemon, driven by RESTful webservice calls
127
+ email: rest-ftp-daemon@bmconseil.com
128
+ executables:
129
+ - rest-ftp-daemon
130
+ extensions: []
131
+ extra_rdoc_files:
132
+ - LICENSE.txt
133
+ - README.md
134
+ files:
135
+ - Gemfile
136
+ - Gemfile.lock
137
+ - LICENSE.txt
138
+ - README.md
139
+ - Rakefile
140
+ - VERSION
141
+ - bin/rest-ftp-daemon
142
+ - lib/config.rb
143
+ - lib/errors.rb
144
+ - lib/rest-ftp-daemon.rb
145
+ - lib/rest-ftp-daemon/config.ru
146
+ - test/helper.rb
147
+ - test/test_rest-ftp-daemon.rb
148
+ homepage: http://github.com/bmedici/rest-ftp-daemon
149
+ licenses:
150
+ - MIT
151
+ post_install_message:
152
+ rdoc_options: []
153
+ require_paths:
154
+ - lib
155
+ required_ruby_version: !ruby/object:Gem::Requirement
156
+ none: false
157
+ requirements:
158
+ - - ! '>='
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ segments:
162
+ - 0
163
+ hash: 3830976928581563014
164
+ required_rubygems_version: !ruby/object:Gem::Requirement
165
+ none: false
166
+ requirements:
167
+ - - ! '>='
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ requirements: []
171
+ rubyforge_project:
172
+ rubygems_version: 1.8.23
173
+ signing_key:
174
+ specification_version: 3
175
+ summary: RESTful FTP client daemon
176
+ test_files: []