leecher 0.2.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.gitignore +5 -4
  2. data/.travis.yml +2 -0
  3. data/Gemfile +1 -2
  4. data/Gemfile.lock +48 -0
  5. data/README +58 -0
  6. data/Rakefile +5 -7
  7. data/bin/leecher +2 -265
  8. data/bin/leecher-aria2-event-hook +12 -0
  9. data/config/amqp.example.yml +28 -0
  10. data/config/arguments.rb +12 -0
  11. data/config/aria2.example.yml +6 -0
  12. data/config/boot.rb +64 -0
  13. data/config/environment.rb +22 -0
  14. data/config/environments/development.rb +2 -0
  15. data/config/environments/production.rb +5 -0
  16. data/config/environments/test.rb +2 -0
  17. data/config/gheed.example.yml +2 -0
  18. data/config/leecher.example.yml +1 -0
  19. data/config/post-daemonize/readme +5 -0
  20. data/config/pre-daemonize/amqp.rb +6 -0
  21. data/config/pre-daemonize/json.rb +3 -0
  22. data/config/pre-daemonize/readme +12 -0
  23. data/config/pre-daemonize/safely.rb +13 -0
  24. data/leecher.gemspec +13 -4
  25. data/lib/leecher.rb +24 -2
  26. data/lib/leecher/aria2/client.rb +65 -0
  27. data/lib/leecher/client.rb +31 -211
  28. data/lib/leecher/gheed/client.rb +35 -0
  29. data/lib/leecher/metalink_queue.rb +68 -0
  30. data/lib/leecher/version.rb +1 -1
  31. data/libexec/leecher-daemon.rb +34 -0
  32. data/script/console +4 -0
  33. data/script/destroy +4 -0
  34. data/script/generate +4 -0
  35. data/spec/aria2/client_spec.rb +26 -0
  36. data/spec/client_spec.rb +42 -83
  37. data/spec/metalink_queue_spec.rb +25 -0
  38. data/spec/spec.opts +2 -0
  39. data/spec/spec_helper.rb +0 -2
  40. data/tasks/rspec.rake +6 -0
  41. metadata +134 -90
  42. data/config/aria2.conf +0 -15
  43. data/config/client.yml +0 -32
  44. data/lib/leecher/log.rb +0 -91
  45. data/lib/leecher/shellout.rb +0 -27
  46. data/spec/shellout_spec.rb +0 -26
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
- *.gem
2
- .bundle
3
- Gemfile.lock
4
- pkg/*
1
+ log
2
+ config/gheed.yml
3
+ config/amqp.yml
4
+ config/aria2.yml
5
+ config/leecher.yml
@@ -0,0 +1,2 @@
1
+ rvm:
2
+ - 1.9.3
data/Gemfile CHANGED
@@ -1,4 +1,3 @@
1
- source "http://rubygems.org"
1
+ source :rubygems
2
2
 
3
- # Specify your gem's dependencies in leecher.gemspec
4
3
  gemspec
@@ -0,0 +1,48 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ leecher (1.0.0)
5
+ amqp
6
+ daemon-kit
7
+ erubis
8
+ multi_json
9
+ safely
10
+ yajl-ruby
11
+
12
+ GEM
13
+ remote: http://rubygems.org/
14
+ specs:
15
+ amq-client (0.9.4)
16
+ amq-protocol (>= 0.9.4)
17
+ eventmachine
18
+ amq-protocol (0.9.4)
19
+ amqp (0.9.7)
20
+ amq-client (~> 0.9.4)
21
+ amq-protocol (>= 0.9.4)
22
+ eventmachine
23
+ daemon-kit (0.1.8.2)
24
+ eventmachine (>= 0.12.10)
25
+ safely (>= 0.3.1)
26
+ diff-lcs (1.1.3)
27
+ erubis (2.7.0)
28
+ eventmachine (0.12.10)
29
+ multi_json (1.3.6)
30
+ rake (0.9.2.2)
31
+ rspec (2.11.0)
32
+ rspec-core (~> 2.11.0)
33
+ rspec-expectations (~> 2.11.0)
34
+ rspec-mocks (~> 2.11.0)
35
+ rspec-core (2.11.0)
36
+ rspec-expectations (2.11.1)
37
+ diff-lcs (~> 1.1.3)
38
+ rspec-mocks (2.11.1)
39
+ safely (0.3.2)
40
+ yajl-ruby (1.1.0)
41
+
42
+ PLATFORMS
43
+ ruby
44
+
45
+ DEPENDENCIES
46
+ leecher!
47
+ rake
48
+ rspec
data/README ADDED
@@ -0,0 +1,58 @@
1
+ daemon-kit README
2
+ ================
3
+
4
+ daemon-kit has generated a skeleton Ruby daemon for you to build on. Please read
5
+ through this file to ensure you get going quickly.
6
+
7
+ Directories
8
+ ===========
9
+
10
+ bin/
11
+ leecher - Stub executable to control your daemon with
12
+
13
+ config/
14
+ Environment configuration files
15
+
16
+ lib/
17
+ Place for your libraries
18
+
19
+ libexec/
20
+ leecher.rb - Your daemon code
21
+
22
+ log/
23
+ Log files based on the environment name
24
+
25
+ spec/
26
+ rspec's home
27
+
28
+ tasks/
29
+ Place for rake tasks
30
+
31
+ vendor/
32
+ Place for unpacked gems and DaemonKit
33
+
34
+ tmp/
35
+ Scratch folder
36
+
37
+
38
+ Logging
39
+ =======
40
+
41
+ One of the biggest issues with writing daemons are getting insight into what your
42
+ daemons are doing. Logging with daemon-kit is simplified as DaemonKit creates log
43
+ files per environment in log.
44
+
45
+ On all environments except production the log level is set to DEBUG, but you can
46
+ toggle the log level by sending the running daemon SIGUSR1 and SIGUSR2 signals.
47
+ SIGUSR1 will toggle between DEBUG and INFO levels, SIGUSR2 will blatantly set the
48
+ level to DEBUG.
49
+
50
+ Bundler
51
+ =======
52
+
53
+ daemon-kit uses bundler to ease the nightmare of dependency loading in Ruby
54
+ projects. daemon-kit and its generators all create/update the Gemfile in the
55
+ root of the daemon. You can satisfy the project's dependencies by running
56
+ `bundle install` from within the project root.
57
+
58
+ For more information on bundler, please see http://github.com/carlhuda/bundler
data/Rakefile CHANGED
@@ -1,10 +1,8 @@
1
- require 'bundler'
1
+ require "bundler"
2
2
  Bundler::GemHelper.install_tasks
3
3
 
4
- require "rspec/core/rake_task"
5
- desc "Run specs"
6
- RSpec::Core::RakeTask.new do |t|
7
- t.rspec_opts = %w(-fs --color)
8
- # t.ruby_opts = %w(-w)
9
- end
4
+ require 'daemon_kit/tasks'
10
5
 
6
+ Dir[File.join(File.dirname(__FILE__), 'tasks/*.rake')].each { |rake| load rake }
7
+
8
+ task default: :spec
@@ -1,268 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- module Leecher
4
- class Cli
3
+ require File.expand_path('../../config/environment', __FILE__)
5
4
 
6
- require "optparse"
7
- require "yaml"
8
- require "erubis"
9
- require "fileutils"
10
- require "leecher/log"
11
- require "leecher/client"
12
-
13
-
14
- RC_OK = 0
15
- RC_USAGE = 1
16
- RC_CONF_MISSING = 2
17
- RC_CATASTROPHE = 99
18
-
19
-
20
- def initialize()
21
- @config_dirname = File.join(ENV["HOME"], ".leecher")
22
- @foreground = false
23
- end
24
-
25
-
26
- def parse_opts(argv)
27
- optparser = OptionParser.new() do |o|
28
- o.banner = "leecher [options]"
29
-
30
- o.separator("")
31
- o.separator("options:")
32
-
33
- o.on("--config=DIR",
34
- "Dir containing settings",
35
- "Default: #{@config_dirname}") do |v|
36
- @config_dirname = v
37
- end
38
-
39
- o.on("-f", "--[no-]foreground",
40
- "Run as a loop in a daemon?",
41
- "Default #{@foreground}") do |v|
42
- @foreground = v
43
- end
44
-
45
- o.on("-q", "--print-queue",
46
- "Print the active queue") do |v|
47
- do_print_queue()
48
- Kernel.exit!(RC_OK)
49
- end
50
-
51
- o.on("-p", "--pause",
52
- "Pause downloads") do |v|
53
- do_pause()
54
- Kernel.exit!(RC_OK)
55
- end
56
-
57
- o.on("-r", "--unpause",
58
- "Unpause downloads") do |v|
59
- do_unpause()
60
- Kernel.exit!(RC_OK)
61
- end
62
-
63
- o.separator("")
64
- o.on("-h", "--help", "You're reading it :-)") do
65
- puts o
66
- Kernel.exit!(RC_USAGE)
67
- end
68
- end
69
-
70
- optparser.parse(argv)
71
- self
72
- end
73
-
74
- def do_getting_started()
75
- log.info("We'll ask you a few questions then write you a config in #{@config_dirname}")
76
- log.info(".. you can change where your config lives with -c/--config")
77
-
78
- STDOUT.puts("\n")
79
- config = { :config_dirname => @config_dirname }
80
- [
81
- [:username, "your username on the website?", :required],
82
- [:password, ".. and the password for that user?", :password],
83
- [:host, "where does the amqp queue live?", "nzb.trim.za.net"],
84
- [:aria2_bin, "which aria2 must I use?", %x{which aria2c}.strip()],
85
- [:aria2_dir, "what whould you like your default download directory to be?", Dir.pwd],
86
- [:aria2_connections, "how many connections would you like to download with?", 5],
87
- [:aria2_rpc_user, "what is your aria2 rpc username?", :required],
88
- [:aria2_rpc_password, "what is your aria2 rpc password?", :password],
89
- [:aria2_rpc_port, "what is your aria2 rpc port?", 6800],
90
- ].each do |setting, question, default|
91
- config[setting] = get_user_input(question, default)
92
- end
93
-
94
- FileUtils.mkdir_p(@config_dirname)
95
- erb = Erubis::Eruby.new(File.read(File.join(File.dirname(__FILE__),
96
- "../config/client.yml")))
97
- File.open(File.join(@config_dirname, "config.yml"), "w") do |f|
98
- f.puts(erb.result(config))
99
- end
100
-
101
- erb = Erubis::Eruby.new(File.read(File.join(File.dirname(__FILE__),
102
- "../config/aria2.conf")))
103
-
104
- File.open(File.join(@config_dirname, "aria2.conf"), "w") do |f|
105
- f.puts(erb.result(config))
106
- end
107
-
108
- STDOUT.puts("\n")
109
- log.info("Looking good, now take us for a real spin!")
110
- end
111
-
112
- # Somewhat lifted from
113
- # http://sourceforge.net/apps/trac/aria2/browser/trunk/doc/xmlrpc/aria2mon
114
- def do_print_queue()
115
- config = load_config(@config_dirname)
116
- client = make_client(config, @log)
117
- log.info("Asking aria2 about it's active downloads ..")
118
- active = client.call_rpc("aria2.tellActive")
119
- active.each do |entry|
120
- gid = entry["gid"]
121
- complete, total, speed = [
122
- "completedLength",
123
- "totalLength",
124
- "downloadSpeed",
125
- ].map do |field|
126
- entry[field].to_i
127
- end
128
- remaining = total - complete
129
- eta_str = if speed == 0
130
- "N/A"
131
- else
132
- remsec = remaining/speed
133
- hr = remsec/3600
134
- remsec = remsec%3600
135
- min = remsec/60
136
- remsec = remsec%60
137
- result = ""
138
- result += "#{hr}h" if hr > 0
139
- result += "#{min}m" if min > 0
140
- result += "#{remsec}s"
141
- end
142
- no_files = entry["files"].size
143
- first_file = entry["files"].first["uris"].first["uri"]
144
- log.info("##{gid}: #{no_files} file(s) (currently: #{File.basename(first_file)}). "\
145
- "#{complete}/#{total} (#{"%.2f" % (complete.to_f * 100 / total)}%). "\
146
- "ETA: #{eta_str}")
147
- end
148
- end
149
-
150
- def do_pause()
151
- config = load_config(@config_dirname)
152
- client = make_client(config, @log)
153
- log.info("Asking aria2 to pause ..")
154
- log.debug(client.call_rpc("aria2.pauseAll"))
155
- end
156
-
157
- def do_unpause()
158
- config = load_config(@config_dirname)
159
- client = make_client(config, @log)
160
- log.info("Asking aria2 to pause ..")
161
- log.debug(client.call_rpc("aria2.unpauseAll"))
162
- end
163
-
164
- def get_user_input(question, default)
165
- default_text = case default
166
- when :required, :password; nil
167
- when :disable; "blank to disable"
168
- else; "default: #{default}"
169
- end
170
-
171
- response = ask([
172
- question,
173
- default_text ? "(#{default_text})" : nil,
174
- ].compact.join(" "),
175
- default == :password)
176
-
177
- if response.empty?
178
- response = case default
179
- when :required, :password
180
- log.error("That option was required! Try again :-)")
181
- get_user_input(question, default)
182
- when :disable
183
- nil
184
- else
185
- default
186
- end
187
- end
188
-
189
- return response
190
- end
191
-
192
- def ask(question, hide_input = false)
193
- STDOUT.write(question + " ")
194
- system("stty -echo") if hide_input
195
- response = STDIN.gets().strip()
196
- if hide_input
197
- STDOUT.puts()
198
- system("stty echo")
199
- end
200
- response
201
- end
202
-
203
- def run()
204
- config = load_config(@config_dirname)
205
- unless @foreground
206
- log.info("Fading into the ether ..")
207
- make_daemon(config[:daemon])
208
- end
209
- client = make_client(config, @log)
210
- client.run()
211
- Kernel.exit(RC_OK)
212
- end
213
-
214
- def log
215
- @log ||= Leecher::Log.new()
216
- end
217
-
218
- def load_config(config_dirname)
219
- unless File.exist?(config_dirname)
220
- log.info("You don't have a leecher config setup")
221
- do_getting_started()
222
- end
223
-
224
- config = YAML.load_file(File.join(config_dirname, "config.yml"))
225
- if (log_conf = config[:log])
226
- @log = Leecher::Log.new(config[:log][:level],
227
- config[:log][:filename])
228
- end
229
- config
230
- rescue Errno::ENOENT
231
- log.error("No such config file #{config_dirname.inspect()}")
232
- Kernel.exit!(RC_CONF_MISSING)
233
- end
234
-
235
- def make_daemon(config)
236
- raise log.fatal("First fork failed") if (pid = Kernel.fork()) == -1
237
- Kernel.exit!(RC_OK) unless pid.nil?
238
- Process.setsid()
239
- File.open(File.join(@config_dirname, "pid"), "w") do |f|
240
- f.puts(Process.pid)
241
- end
242
- Dir.chdir(config[:chdir]) if config.has_key? :chdir
243
- File.umask(config[:umask]) if config.has_key? :umask
244
- [
245
- [STDIN, :stdin],
246
- [STDOUT, :stdout],
247
- [STDERR, :stderr],
248
- ].each do |io, key|
249
- if config.has_key? key
250
- io.reopen(*config[key])
251
- else
252
- io.reopen("/dev/null")
253
- end
254
- end
255
- end
256
-
257
- def make_client(config, log)
258
- Leecher::Client.new(config[:username],
259
- config[:password],
260
- config[:metalink_queue_name],
261
- config[:bunny_opts],
262
- config[:aria2_opts],
263
- log)
264
- end
265
- end
266
- end
267
-
268
- Leecher::Cli.new().parse_opts(ARGV).run()
5
+ DaemonKit::Application.exec(DAEMON_ROOT + '/libexec/leecher-daemon.rb')
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../../config/environment', __FILE__)
4
+
5
+ leecher = Leecher.new
6
+
7
+ unless ARGV.size == 3
8
+ raise ArgumentError.new("#{ARGV.inspect} should be in format: [gid, number-of-files, file-path]")
9
+ end
10
+
11
+ gid, no_files, file_path = ARGV
12
+ leecher.state_change(Integer(gid))
@@ -0,0 +1,28 @@
1
+ # AMQP client configuration file
2
+
3
+ # These values will be used to configure the ampq gem, any values
4
+ # omitted will let the gem use it's own defaults.
5
+
6
+ # The configuration specifies the following keys:
7
+ # * user - Username for the broker
8
+ # * pass - Password for the broker
9
+ # * host - Hostname where the broker is running
10
+ # * vhost - Vhost to connect to
11
+ # * port - Port where the broker is running
12
+ # * ssl - Use ssl or not
13
+ # * timeout - Timeout
14
+
15
+ defaults: &defaults
16
+ user: guest
17
+ pass: guest
18
+ host: localhost
19
+ vhost: /
20
+
21
+ development:
22
+ <<: *defaults
23
+
24
+ test:
25
+ <<: *defaults
26
+
27
+ production:
28
+ <<: *defaults