leecher 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in leecher.gemspec
4
+ gemspec
@@ -0,0 +1,10 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
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
10
+
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Leecher
4
+ class Cli
5
+
6
+ require "optparse"
7
+ require "yaml"
8
+ require "erubis"
9
+ require "leecher/log"
10
+ require "leecher/client"
11
+
12
+
13
+ RC_OK = 0
14
+ RC_USAGE = 1
15
+ RC_CONF_MISSING = 2
16
+ RC_CATASTROPHE = 99
17
+
18
+
19
+ def initialize()
20
+ @config_fn = File.join(ENV["HOME"], ".leecher.yml")
21
+ @foreground = false
22
+ end
23
+
24
+
25
+ def parse_opts(argv)
26
+ optparser = OptionParser.new() do |o|
27
+ o.banner = "leecher [options]"
28
+
29
+ o.separator("")
30
+ o.separator("options:")
31
+
32
+ o.on("--config=FILE",
33
+ "File containing settings",
34
+ "Default: #{@config_fn}") do |v|
35
+ @config_fn = v
36
+ end
37
+
38
+ o.on("-f", "--[no-]foreground",
39
+ "Run as a loop in a daemon?",
40
+ "Default #{@foreground}") do |v|
41
+ @foreground = v
42
+ end
43
+
44
+ o.on("--getting-started",
45
+ "Run a tutorial which will give you a config") do
46
+ do_getting_started()
47
+ Kernel.exit!(RC_OK)
48
+ end
49
+
50
+ o.separator("")
51
+ o.on("-h", "--help", "You're reading it :-)") do
52
+ puts o
53
+ Kernel.exit!(RC_USAGE)
54
+ end
55
+ end
56
+
57
+ optparser.parse(argv)
58
+ self
59
+ end
60
+
61
+ def do_getting_started()
62
+ log.info("We'll ask you a few questions then write you a config in #{@config_fn}")
63
+ log.info(".. you can change where your config lives with -c/--config")
64
+
65
+ STDOUT.puts("\n\n")
66
+ config = Hash.new()
67
+ [
68
+ [:username, "your username on the website?", :required],
69
+ [:password, ".. and the password for that user?", :password],
70
+ [:log_fn, "filename to log to:", :disable],
71
+ [:host, "where does the amqp queue live?", "nzb.trim.za.net"],
72
+ [:aria2_bin, "which aria2 must I use?", %x{which aria2c}.strip()],
73
+ ].each do |setting, question, default|
74
+ config[setting] = get_user_input(question, default)
75
+ end
76
+
77
+ erb = Erubis::Eruby.new(File.read(File.join(File.dirname(__FILE__),
78
+ "../config/client.yml")))
79
+ File.open(@config_fn, "w") do |f|
80
+ f.puts(erb.result(config))
81
+ end
82
+
83
+ STDOUT.puts("\n\n")
84
+ log.info("Looking good, now take us for a real spin!")
85
+ end
86
+
87
+ def get_user_input(question, default)
88
+ default_text = case default
89
+ when :required, :password; nil
90
+ when :disable; "blank to disable"
91
+ else; "default: #{default}"
92
+ end
93
+
94
+ response = ask([
95
+ question,
96
+ default_text ? "(#{default_text})" : nil,
97
+ ].compact.join(" "),
98
+ default == :password)
99
+
100
+ if response.empty?
101
+ response = case default
102
+ when :required, :password
103
+ log.error("That option was required! Try again :-)")
104
+ get_user_input(question, default)
105
+ when :disable
106
+ nil
107
+ else
108
+ default
109
+ end
110
+ end
111
+
112
+ return response
113
+ end
114
+
115
+ def ask(question, hide_input = false)
116
+ STDOUT.write(question + " ")
117
+ system("stty -echo") if hide_input
118
+ response = STDIN.gets().strip()
119
+ if hide_input
120
+ STDOUT.puts()
121
+ system("stty echo")
122
+ end
123
+ response
124
+ end
125
+
126
+ def run()
127
+ config = load_config(@config_fn)
128
+ unless @foreground
129
+ log.info("Fading into the ether ..")
130
+ make_daemon(config[:daemon])
131
+ end
132
+ client = Leecher::Client.new(config[:username],
133
+ config[:password],
134
+ config[:metalink_queue_name],
135
+ config[:bunny_opts],
136
+ config[:aria2_opts],
137
+ @log)
138
+ client.run()
139
+ Kernel.exit(RC_OK)
140
+ end
141
+
142
+ def log
143
+ @log ||= Leecher::Log.new()
144
+ end
145
+
146
+ def load_config(config_fn)
147
+ config = YAML.load_file(config_fn)
148
+ if (log_conf = config[:log])
149
+ @log = Leecher::Log.new(config[:log][:level],
150
+ config[:log][:filename])
151
+ end
152
+ config
153
+ rescue Errno::ENOENT
154
+ log.error("No such config file #{config_fn.inspect()}")
155
+ log.info("New user? Try #{Leecher::Log::ANSI_GREEN}"\
156
+ "--getting-started#{Leecher::Log::ANSI_NORMAL}")
157
+ Kernel.exit!(RC_CONF_MISSING)
158
+ end
159
+
160
+ def make_daemon(config)
161
+ raise log.fatal("First fork failed") if (pid = Kernel.fork()) == -1
162
+ Kernel.exit!(RC_OK) unless pid.nil?
163
+ Process.setsid()
164
+ Dir.chdir(config[:chdir]) if config.has_key? :chdir
165
+ File.umask(config[:umask]) if config.has_key? :umask
166
+ [
167
+ [STDIN, :stdin],
168
+ [STDOUT, :stdout],
169
+ [STDERR, :stderr],
170
+ ].each do |io, key|
171
+ if config.has_key? key
172
+ io.reopen(*config[key])
173
+ else
174
+ io.reopen("/dev/null")
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ Leecher::Cli.new().parse_opts(ARGV).run()
@@ -0,0 +1,29 @@
1
+ :username: <%= username %>
2
+ :password: <%= password %>
3
+ :metalink_queue_name: metalink
4
+ :log:
5
+ :level: :info
6
+ :filename: <%= log_fn %>
7
+ :bunny_opts:
8
+ :host: <%= host %>
9
+ :port: 5672
10
+ :vhost: /
11
+ :user: <%= username %>
12
+ :pass: <%= password %>
13
+ :ssl: false
14
+ :verify_ssl: false
15
+ :logfile: false
16
+ :logfile: false
17
+ :frame_max: 131072
18
+ :channel_max: 0
19
+ :heartbeat: 300
20
+ :connect_timeout: 5
21
+ :aria2_opts:
22
+ :bin: <%= aria2_bin %>
23
+ :args: [-x5, -V, --follow-metalink=mem]
24
+ :daemon:
25
+ :chdir: nil
26
+ :umask: 0000
27
+ :stdin: nil
28
+ :stdout: nil
29
+ :stderr: nil
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "leecher/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "leecher"
7
+ s.version = Leecher::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Marc Bowes"]
10
+ s.email = ["marcbowes+leecher@gmail.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Download files off an AMQP queue}
13
+ s.description = %q{A server populates a queue, this client downloads those files}
14
+
15
+ s.rubyforge_project = "leecher"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_development_dependency(%q<rspec>, [">=2"])
23
+ s.add_runtime_dependency(%q<bunny>)
24
+ s.add_runtime_dependency(%q<erubis>)
25
+ end
@@ -0,0 +1,5 @@
1
+ module Leecher
2
+
3
+ require "leecher/version"
4
+ require "leecher/client"
5
+ end
@@ -0,0 +1,125 @@
1
+ module Leecher
2
+
3
+ class Client
4
+
5
+ require "bunny"
6
+ require "leecher/shellout"
7
+
8
+ include Leecher::Shellout
9
+
10
+ class ShutdownError < RuntimeError; end
11
+
12
+ attr_accessor :username
13
+ attr_accessor :password
14
+ attr_accessor :metalink_queue_name
15
+ attr_accessor :bunny_opts
16
+ attr_accessor :aria2_opts
17
+ attr_accessor :log
18
+
19
+ def initialize(username,
20
+ password,
21
+ metalink_queue_name,
22
+ bunny_opts,
23
+ aria2_opts,
24
+ log)
25
+ self.username = username
26
+ self.password = password
27
+ self.metalink_queue_name = metalink_queue_name
28
+ self.bunny_opts = bunny_opts
29
+ self.aria2_opts = aria2_opts
30
+ self.log = log
31
+ end
32
+
33
+
34
+ def run()
35
+ @bunny = make_bunny(self.bunny_opts)
36
+ @bunny.start()
37
+
38
+ @graceful_shutdown = false
39
+ @dont_kill_me = false
40
+ [:INT, :TERM].each do |sig|
41
+ trap(sig) do
42
+ @graceful_shutdown = true
43
+ log.info("Caught SIG{sig}")
44
+ raise ShutdownError.new("SIG#{sig}") unless @dont_kill_me
45
+ end
46
+ end
47
+
48
+ q = get_queue(@bunny, username, metalink_queue_name)
49
+ until @graceful_shutdown
50
+ begin
51
+ drain_metalink_queue(q)
52
+ rescue ShutdownError => e
53
+ log.info(["Going away because of", e.message].join(": "))
54
+ rescue Exception => e
55
+ log.error([e.class.name, e.message].join(": "))
56
+ e.backtrace.each do |line|
57
+ log.debug(line)
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ def make_bunny(bunny_opts)
64
+ Bunny.new(bunny_opts)
65
+ end
66
+
67
+ def get_queue(bunny, *queue_joinable_name)
68
+ bunny.queue(File.join(*queue_joinable_name))
69
+ end
70
+
71
+ def drain_metalink_queue(q)
72
+ next_queue_message(q) do |msg|
73
+ @dont_kill_me = true
74
+
75
+ aria_exec = make_aria_exec(URI.parse(msg))
76
+ log.info("Will exec #{aria_exec.inspect()}")
77
+ stdout_rd, stdout_wr = IO.pipe()
78
+ stderr_rd, stderr_wr = IO.pipe()
79
+ rc = shellout(aria_exec, nil, stdout_wr, stderr_wr)
80
+ stdout_wr.close()
81
+ stderr_wr.close()
82
+
83
+ if rc == 0
84
+ log.info("aria2c exited happily")
85
+ return true
86
+ else
87
+ log.error("aria2c exited unhappily (rc = #{rc})")
88
+ stdout_rd.each_line do |line|
89
+ log.error(line)
90
+ end
91
+ stderr_rd.each_line do |line|
92
+ log.debug(line)
93
+ end
94
+ return false
95
+ end
96
+
97
+ @dont_kill_me = false
98
+ end
99
+ end
100
+
101
+ def next_queue_message(q, &block)
102
+ q.subscribe() do |msg|
103
+ payload = msg[:payload]
104
+ case payload
105
+ when :queue_empty
106
+ next
107
+ else
108
+ if block_given?
109
+ yield(payload)
110
+ else
111
+ payload
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ def make_aria_exec(uri)
118
+ [
119
+ aria2_opts[:bin],
120
+ aria2_opts[:args].join(" "),
121
+ uri.to_s(),
122
+ ].join(" ")
123
+ end
124
+ end # class Client
125
+ end # module Leecher
@@ -0,0 +1,89 @@
1
+ module Leecher
2
+
3
+ class Log
4
+
5
+ require "time"
6
+
7
+ LEVELS = {
8
+ :debug => 0,
9
+ :warn => 1,
10
+ :info => 2,
11
+ :error => 3,
12
+ :fatal => 4,
13
+ }
14
+
15
+ attr_accessor :log_fn
16
+ attr_reader :level
17
+
18
+ def initialize(level = :debug,
19
+ log_fn = nil)
20
+ self.level = level
21
+ self.log_fn = log_fn
22
+ open_log_fd()
23
+ end
24
+
25
+ def open_log_fd
26
+ @log_fd.close() if @log_fd and not @log_fd.closed?
27
+ if self.log_fn
28
+ @log_fd = File.open(self.log_fn, "a")
29
+ end
30
+ end
31
+
32
+ def close_log_fd
33
+ if @log_fd and not @log_fd.closed?
34
+ @log_fd.flush()
35
+ @log_fd.close()
36
+ end
37
+ end
38
+
39
+ LEVELS.each_key do |level|
40
+ define_method(level) do |message|
41
+ write_to_log(level, message)
42
+ end
43
+ end
44
+
45
+ def level=(level)
46
+ @level = level
47
+ @_enum_level = LEVELS[level]
48
+ end
49
+
50
+ def write_to_log(level, message)
51
+ if LEVELS[level] >= @_enum_level
52
+ do_write_to_log(level, message)
53
+ else
54
+ # Toss it
55
+ end
56
+ end
57
+
58
+ def do_write_to_log(level, message)
59
+ iso8601_timestr = Time.now.iso8601()
60
+ log_for_humans(iso8601_timestr, level, message)
61
+
62
+ if @log_fd and not @log_fd.closed?
63
+ @log_fd.puts("%s %s %s" % [iso8601_timestr, level.to_s.upcase, message])
64
+ end
65
+ end
66
+
67
+ # Map the log level (LOG_LEVEL_*) to an array containing
68
+ # [human_name:String, colour:String(ANSI escape sequence)]
69
+ ANSI_RED = "\033[0;31m"
70
+ ANSI_RED_INVERTED = "\033[7;31m"
71
+ ANSI_BROWN = "\033[0;33m"
72
+ ANSI_MAGENTA = "\033[0;35m"
73
+ ANSI_GREEN = "\033[0;32m"
74
+ ANSI_BOLD_WHITE = "\033[0;37m"
75
+ ANSI_NORMAL = "\033[0m"
76
+ HUMAN_LOG_LEVELS = {
77
+ :fatal => ["FATAL", ANSI_RED_INVERTED],
78
+ :error => ["ERROR", ANSI_RED],
79
+ :info => ["INFO", ANSI_GREEN],
80
+ :warn => ["WARN", ANSI_MAGENTA],
81
+ :debug => ["DEBUG", ANSI_BROWN],
82
+ }
83
+ def log_for_humans(iso8601_timestr, log_level, log_msg)
84
+ level_str, level_colour = HUMAN_LOG_LEVELS[log_level]
85
+ puts("#{ANSI_BOLD_WHITE}#{iso8601_timestr}#{ANSI_NORMAL} "\
86
+ "#{level_colour}#{"%5.5s" % level_str}#{ANSI_NORMAL} #{log_msg}")
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,27 @@
1
+ module Leecher
2
+ module Shellout
3
+
4
+ # FIXME: probably want to implement timeouts here..
5
+ def shellout(cmd, stdin = nil, stdout = nil, stderr = nil)
6
+ child_pid, child_status = nil
7
+ child_pid = Kernel.fork()
8
+
9
+ if child_pid
10
+ # Parent process executes this
11
+ child_status = (Process.waitpid2(child_pid)[1]).to_i >> 8
12
+ else
13
+ Process.setsid()
14
+ STDIN.reopen(stdin || "/dev/null")
15
+ STDOUT.reopen(stdout || "/dev/null")
16
+ STDERR.reopen(stderr || "/dev/null")
17
+ 3.upto(256) { |fd| IO.new(fd).close rescue nil }
18
+
19
+ Kernel.exec(cmd)
20
+ # Never reached.
21
+ Kernel.exit!
22
+ end
23
+
24
+ child_status
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ module Leecher
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,74 @@
1
+ require File.dirname(__FILE__) + "/spec_helper"
2
+
3
+ require "lib/leecher/client"
4
+
5
+ describe Leecher::Client do
6
+
7
+ before do
8
+ mock_log = double("log")
9
+ %w(debug warn info error fatal).each do |log_level|
10
+ mock_log.should_receive(log_level).any_number_of_times
11
+ end
12
+ @c = Leecher::Client.new("username",
13
+ "password",
14
+ "metalink",
15
+ double("bunny_opts"),
16
+ {
17
+ :bin => "/usr/bin/aria2c",
18
+ :args => ["-V"],
19
+ },
20
+ mock_log)
21
+ end
22
+
23
+
24
+ it "should initialize" do
25
+ @c.username.should == "username"
26
+ @c.password.should == "password"
27
+ @c.metalink_queue_name.should == "metalink"
28
+ end
29
+
30
+ describe "#next_queue_message" do
31
+
32
+ it "should only return when something is on the queue" do
33
+ bunny = double("bunny")
34
+ @c.stub(:make_bunny) { bunny }
35
+ q = double("q")
36
+ q.should_receive(:subscribe).
37
+ and_yield({:payload => :queue_empty}).
38
+ and_yield({:payload => "message"})
39
+ @c.next_queue_message(q).should == "message"
40
+ end
41
+ end
42
+
43
+ describe "#drain_metalink_queue" do
44
+
45
+ it "should reject URIs that are not URIs" do
46
+ @c.stub(:next_queue_message).
47
+ and_yield(nil)
48
+ lambda { @c.drain_metalink_queue(double("q")) }.
49
+ should raise_error(URI::InvalidURIError)
50
+ end
51
+
52
+ it "should shellout to aria2" do
53
+ @c.stub(:next_queue_message).
54
+ and_yield("http://some/url")
55
+ @c.should_receive(:shellout).
56
+ with("/usr/bin/aria2c -V http://some/url",
57
+ anything(), anything(), anything()).
58
+ and_return(0)
59
+ @c.drain_metalink_queue(double("q")).should be_true
60
+ end
61
+
62
+ it "should return according to the rc" do
63
+ @c.stub(:next_queue_message).
64
+ and_yield("http://some/url")
65
+ @c.stub(:shellout).
66
+ and_return(0)
67
+ q = double("q")
68
+ @c.drain_metalink_queue(q).should be_true
69
+ @c.stub(:shellout).
70
+ and_return(1)
71
+ @c.drain_metalink_queue(q).should be_false
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + "../lib")
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: leecher
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Marc Bowes
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-04-22 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rspec
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 7
30
+ segments:
31
+ - 2
32
+ version: "2"
33
+ type: :development
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: bunny
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: erubis
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :runtime
62
+ version_requirements: *id003
63
+ description: A server populates a queue, this client downloads those files
64
+ email:
65
+ - marcbowes+leecher@gmail.com
66
+ executables:
67
+ - leecher
68
+ extensions: []
69
+
70
+ extra_rdoc_files: []
71
+
72
+ files:
73
+ - .gitignore
74
+ - Gemfile
75
+ - Rakefile
76
+ - bin/leecher
77
+ - config/client.yml
78
+ - leecher.gemspec
79
+ - lib/leecher.rb
80
+ - lib/leecher/client.rb
81
+ - lib/leecher/log.rb
82
+ - lib/leecher/shellout.rb
83
+ - lib/leecher/version.rb
84
+ - spec/client_spec.rb
85
+ - spec/spec_helper.rb
86
+ has_rdoc: true
87
+ homepage: ""
88
+ licenses: []
89
+
90
+ post_install_message:
91
+ rdoc_options: []
92
+
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ hash: 3
101
+ segments:
102
+ - 0
103
+ version: "0"
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ hash: 3
110
+ segments:
111
+ - 0
112
+ version: "0"
113
+ requirements: []
114
+
115
+ rubyforge_project: leecher
116
+ rubygems_version: 1.6.2
117
+ signing_key:
118
+ specification_version: 3
119
+ summary: Download files off an AMQP queue
120
+ test_files: []
121
+