quartz_flow 0.0.1
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.
- data/bin/createdb.rb +30 -0
- data/bin/quartzflow +177 -0
- data/etc/logging.rb +12 -0
- data/etc/quartz.rb +25 -0
- data/lib/quartz_flow/authentication.rb +88 -0
- data/lib/quartz_flow/home.rb +76 -0
- data/lib/quartz_flow/mock_client.rb +247 -0
- data/lib/quartz_flow/model.rb +27 -0
- data/lib/quartz_flow/randstring.rb +10 -0
- data/lib/quartz_flow/server.rb +305 -0
- data/lib/quartz_flow/session.rb +83 -0
- data/lib/quartz_flow/settings_helper.rb +154 -0
- data/lib/quartz_flow/torrent_manager.rb +247 -0
- data/lib/quartz_flow/usagetracker.rb +335 -0
- data/lib/quartz_flow/wrappers.rb +124 -0
- data/public/bootstrap/css/bootstrap-responsive.css +1109 -0
- data/public/bootstrap/css/bootstrap-responsive.min.css +9 -0
- data/public/bootstrap/css/bootstrap.css +6167 -0
- data/public/bootstrap/css/bootstrap.min.css +9 -0
- data/public/bootstrap/img/glyphicons-halflings-white.png +0 -0
- data/public/bootstrap/img/glyphicons-halflings.png +0 -0
- data/public/bootstrap/js/bootstrap.js +2280 -0
- data/public/bootstrap/js/bootstrap.min.js +6 -0
- data/public/js/jquery-1.10.2.min.js +6 -0
- data/public/js/quartz.js +419 -0
- data/public/style.css +25 -0
- data/views/config_partial.haml +30 -0
- data/views/index.haml +21 -0
- data/views/login.haml +32 -0
- data/views/torrent_detail_partial.haml +91 -0
- data/views/torrent_table_partial.haml +76 -0
- metadata +157 -0
data/bin/createdb.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'fileutils'
|
3
|
+
require 'quartz_flow/model'
|
4
|
+
|
5
|
+
DataMapper::Logger.new($stdout, :debug)
|
6
|
+
|
7
|
+
$settings = {}
|
8
|
+
def set(sym, value)
|
9
|
+
puts "Set called: #{sym}=#{value}"
|
10
|
+
$settings[sym] = value
|
11
|
+
end
|
12
|
+
|
13
|
+
require './etc/quartz'
|
14
|
+
|
15
|
+
dbPath = "#{Dir.pwd}/#{$settings[:db_file]}"
|
16
|
+
path = "sqlite://#{dbPath}"
|
17
|
+
DataMapper.setup(:default, path)
|
18
|
+
|
19
|
+
dir = File.dirname($settings[:db_file])
|
20
|
+
FileUtils.mkdir dir if dir.length > 0 && ! File.directory?(dir)
|
21
|
+
|
22
|
+
|
23
|
+
if ! File.exists?(dbPath)
|
24
|
+
puts "creating database #{path}"
|
25
|
+
DataMapper.auto_migrate!
|
26
|
+
else
|
27
|
+
puts "upgrading database #{path}"
|
28
|
+
DataMapper.auto_upgrade!
|
29
|
+
end
|
30
|
+
|
data/bin/quartzflow
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'getoptlong'
|
3
|
+
require 'quartz_torrent'
|
4
|
+
|
5
|
+
def doSetup
|
6
|
+
home = "."
|
7
|
+
|
8
|
+
opts = GetoptLong.new(
|
9
|
+
[ '--homedir', '-d', GetoptLong::REQUIRED_ARGUMENT],
|
10
|
+
)
|
11
|
+
|
12
|
+
opts.each do |opt, arg|
|
13
|
+
if opt == '--homedir'
|
14
|
+
home = arg
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
if ! File.directory?(home)
|
19
|
+
puts "Error: The directory '#{home}' we will setup into doesn't exist."
|
20
|
+
exit 1
|
21
|
+
end
|
22
|
+
|
23
|
+
if ! File.owned?(home)
|
24
|
+
puts "Error: The directory '#{home}' is not owned by this user. Please change the owner."
|
25
|
+
exit 1
|
26
|
+
end
|
27
|
+
|
28
|
+
puts "Initializing new quartzflow home in #{home == "." ? "current directory" : home}"
|
29
|
+
Home.new(home).setup
|
30
|
+
end
|
31
|
+
|
32
|
+
def doHelp
|
33
|
+
if ARGV.size == 0
|
34
|
+
puts "Used to manage or start quartzflow."
|
35
|
+
puts "Usage: #{$0} [command] [options]"
|
36
|
+
puts ""
|
37
|
+
puts "If no command is specified, start is assumed."
|
38
|
+
puts "For help with a specific command, use 'help command'. The available commands are:"
|
39
|
+
puts " start Start quartzflow"
|
40
|
+
puts " setup Setup a new quartzflow home directory"
|
41
|
+
puts " adduser Add a user"
|
42
|
+
else
|
43
|
+
command = ARGV.shift
|
44
|
+
if command == "setup"
|
45
|
+
puts "Initialize a new quartzflow home directory. This command creates the necessary "
|
46
|
+
puts "subdirectories and files needed for quartzflow to run. When run without options, "
|
47
|
+
puts "the current directory is set up. The directory should be empty and owned by the "
|
48
|
+
puts "current user."
|
49
|
+
puts ""
|
50
|
+
puts "Options:"
|
51
|
+
puts " --homedir DIR, -d DIR Setup DIR instead of the current directory"
|
52
|
+
elsif command == "start"
|
53
|
+
puts "Start quartzflow. When run without options, the current directory should be a"
|
54
|
+
puts "quartzflow home directory, as created using the setup command."
|
55
|
+
puts ""
|
56
|
+
puts "Options:"
|
57
|
+
puts " --noauth, -n Disable user authentication (users won't be "
|
58
|
+
puts " prompted for a password)"
|
59
|
+
elsif command == "adduser"
|
60
|
+
puts "Add a user. This command must be run with the --login option."
|
61
|
+
puts ""
|
62
|
+
puts "Options:"
|
63
|
+
puts " --login LOGIN, -l LOGIN Login name for the user to be added"
|
64
|
+
else
|
65
|
+
puts "Unknown command"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def doAddUser
|
71
|
+
isLinux = RUBY_PLATFORM.downcase.include?("linux")
|
72
|
+
|
73
|
+
opts = GetoptLong.new(
|
74
|
+
[ '--login', '-l', GetoptLong::REQUIRED_ARGUMENT],
|
75
|
+
)
|
76
|
+
|
77
|
+
login = nil
|
78
|
+
opts.each do |opt, arg|
|
79
|
+
if opt == '--login'
|
80
|
+
login = arg
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
if ! login
|
85
|
+
puts "The --login option is required for the adduser command"
|
86
|
+
exit 1
|
87
|
+
end
|
88
|
+
|
89
|
+
system "stty -echo" if isLinux
|
90
|
+
|
91
|
+
print "Password: "
|
92
|
+
$stdout.flush
|
93
|
+
passwd1 = $stdin.gets.strip
|
94
|
+
puts
|
95
|
+
print "Password again: "
|
96
|
+
$stdout.flush
|
97
|
+
passwd2 = $stdin.gets.strip
|
98
|
+
puts
|
99
|
+
|
100
|
+
system "stty echo" if isLinux
|
101
|
+
|
102
|
+
if passwd1 != passwd2
|
103
|
+
puts "Passwords don't match"
|
104
|
+
exit 1
|
105
|
+
end
|
106
|
+
|
107
|
+
auth = Authentication.new("etc/passwd")
|
108
|
+
auth.add_account(login, passwd1)
|
109
|
+
end
|
110
|
+
|
111
|
+
def doStart
|
112
|
+
QuartzTorrent.initThread("main")
|
113
|
+
|
114
|
+
opts = GetoptLong.new(
|
115
|
+
[ '--noauth', '-n', GetoptLong::NO_ARGUMENT],
|
116
|
+
)
|
117
|
+
|
118
|
+
$useAuthentication = true
|
119
|
+
opts.each do |opt, arg|
|
120
|
+
if opt == '--noauth'
|
121
|
+
$useAuthentication = false
|
122
|
+
puts "Warning: running without user authentication. This is not secure"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
require 'quartz_flow/server'
|
127
|
+
|
128
|
+
# There is an interaction with Sinatra where we can't register signal handlers until
|
129
|
+
# Sinatra has started (and thus performed it's own registration/reset of the handlers).
|
130
|
+
# The workaround is to wait for the server to start before registering.
|
131
|
+
Thread.new do
|
132
|
+
begin
|
133
|
+
puts "Starting thread"
|
134
|
+
stdout = $stdout
|
135
|
+
io = File.open('/dev/pts/0','w')
|
136
|
+
until Server.running?
|
137
|
+
sleep 1
|
138
|
+
end
|
139
|
+
|
140
|
+
if Signal.list.has_key?('USR1')
|
141
|
+
puts "Registering SIGUSR1 handler"
|
142
|
+
Signal.trap('SIGUSR1') { QuartzTorrent.logBacktraces($stdout) }
|
143
|
+
Signal.trap('SIGQUIT') { QuartzTorrent.logBacktraces($stdout) }
|
144
|
+
end
|
145
|
+
rescue
|
146
|
+
puts $!
|
147
|
+
puts $!.backtrace.join "\n"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
Server.run!
|
152
|
+
end
|
153
|
+
|
154
|
+
command = :start
|
155
|
+
|
156
|
+
if ARGV.size > 0
|
157
|
+
command = ARGV.shift.to_sym
|
158
|
+
end
|
159
|
+
|
160
|
+
if command == :setup
|
161
|
+
# home is slow to load since it loads the model and calls DataMapper.finalize, so we require it only when needed
|
162
|
+
require 'quartz_flow/home'
|
163
|
+
doSetup
|
164
|
+
elsif command == :start
|
165
|
+
# home is slow to load since it loads the model and calls DataMapper.finalize, so we require it only when needed
|
166
|
+
require 'quartz_flow/home'
|
167
|
+
exit 1 if ! Home.new.validate
|
168
|
+
|
169
|
+
doStart
|
170
|
+
elsif command == :help
|
171
|
+
doHelp
|
172
|
+
elsif command == :adduser
|
173
|
+
require 'quartz_flow/authentication'
|
174
|
+
doAddUser
|
175
|
+
else
|
176
|
+
puts "Unknown command #{command}"
|
177
|
+
end
|
data/etc/logging.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# Configure logging levels. Default level for all loggers is :info.
|
2
|
+
|
3
|
+
#set "peerclient", :debug
|
4
|
+
#set "peer_manager", :debug
|
5
|
+
#set "tracker_client", :debug
|
6
|
+
#set "http_tracker_client", :debug
|
7
|
+
#set "udp_tracker_client", :debug
|
8
|
+
#set "peerclient", :debug
|
9
|
+
#set "peerclient.reactor", :debug
|
10
|
+
#set "blockstate", :debug
|
11
|
+
#set "piecemanager", :debug
|
12
|
+
#set "peerholder", :debug
|
data/etc/quartz.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# Quartzflow config file.
|
2
|
+
|
3
|
+
# IP address that the web server should bind to
|
4
|
+
set :bind, "0.0.0.0"
|
5
|
+
|
6
|
+
# TCP port that the web server should bind to
|
7
|
+
set :port, 4444
|
8
|
+
|
9
|
+
# Directory where downloaded torrent data will be stored
|
10
|
+
set :basedir, "download"
|
11
|
+
|
12
|
+
# Directory where .torrent files and .info files will be stored.
|
13
|
+
set :metadir, "meta"
|
14
|
+
|
15
|
+
# TCP port used for torrents
|
16
|
+
set :torrent_port, 9997
|
17
|
+
|
18
|
+
# SQLite database used for storing settings and state
|
19
|
+
set :db_file, "db/quartz.sqlite"
|
20
|
+
|
21
|
+
# Where to log torrent protocol messages
|
22
|
+
set :torrent_log, "log/torrent.log"
|
23
|
+
|
24
|
+
# On which day of the month should monthly usage tracking reset
|
25
|
+
set :monthly_usage_reset_day, 5
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'quartz_flow/randstring'
|
4
|
+
|
5
|
+
class AccountInfo
|
6
|
+
def initialize(login = nil, password_hash = nil, salt = nil)
|
7
|
+
@login = login
|
8
|
+
@password_hash = password_hash
|
9
|
+
@salt = salt
|
10
|
+
end
|
11
|
+
attr_accessor :login
|
12
|
+
attr_accessor :password_hash
|
13
|
+
attr_accessor :salt
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
class Authentication
|
18
|
+
def initialize(password_file)
|
19
|
+
@password_file = password_file
|
20
|
+
@accounts = {}
|
21
|
+
load_password_file(password_file)
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_account(login, unhashed_password)
|
25
|
+
if @accounts.has_key?login
|
26
|
+
raise "The account #{login} already exists"
|
27
|
+
end
|
28
|
+
raise "Password cannot be empty" if unhashed_password.nil?
|
29
|
+
add_account_internal(login, unhashed_password)
|
30
|
+
end
|
31
|
+
|
32
|
+
def del_account(login)
|
33
|
+
if ! @accounts.has_key?(login)
|
34
|
+
raise "The account #{login} does not exist"
|
35
|
+
end
|
36
|
+
del_account_internal(login)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns true on success, false if the user cannot be authenticated
|
40
|
+
def authenticate(login, password)
|
41
|
+
# Reload the password file in case users were added/deleted
|
42
|
+
acct = @accounts[login]
|
43
|
+
return false if ! acct
|
44
|
+
hashed = hash_password(password, acct.salt)
|
45
|
+
hashed == acct.password_hash
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def load_password_file(filename)
|
51
|
+
if File.exists? filename
|
52
|
+
File.open(filename, "r") do |file|
|
53
|
+
@accounts.clear
|
54
|
+
file.each_line do |line|
|
55
|
+
if line =~ /([^:]+):(.*):(.*)/
|
56
|
+
@accounts[$1] = AccountInfo.new($1,$2,$3)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_account_internal(login, unhashed_password)
|
64
|
+
salt = RandString.make_random_string(10)
|
65
|
+
acct = AccountInfo.new(login, hash_password(unhashed_password, salt), salt)
|
66
|
+
File.open(@password_file, "a") do |file|
|
67
|
+
file.puts "#{login}:#{acct.password_hash}:#{salt}"
|
68
|
+
end
|
69
|
+
@accounts[login] = acct
|
70
|
+
end
|
71
|
+
|
72
|
+
def hash_password(pass, salt)
|
73
|
+
Digest::SHA256.hexdigest(pass + salt)
|
74
|
+
end
|
75
|
+
|
76
|
+
def del_account_internal(login)
|
77
|
+
tmpfile = "#{@password_file}.new"
|
78
|
+
File.open(tmpfile, "w") do |outfile|
|
79
|
+
File.open(@password_file, "r") do |infile|
|
80
|
+
infile.each_line do |line|
|
81
|
+
outfile.print line if line !~ /^#{login}:/
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
FileUtils.mv tmpfile, @password_file
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'quartz_flow/model'
|
3
|
+
|
4
|
+
# Class used to setup a new QuartzFlow home directory, and get information about it.
|
5
|
+
class Home
|
6
|
+
def initialize(dir = ".")
|
7
|
+
@dir = dir
|
8
|
+
|
9
|
+
@subdirs = [
|
10
|
+
"etc",
|
11
|
+
"log",
|
12
|
+
"download",
|
13
|
+
"meta",
|
14
|
+
"public",
|
15
|
+
"db",
|
16
|
+
"views",
|
17
|
+
]
|
18
|
+
|
19
|
+
@installRoot = Home.determineAppRoot("quartz_flow")
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate
|
23
|
+
rc = true
|
24
|
+
@subdirs.each do |subdir|
|
25
|
+
path = File.join(@dir, subdir)
|
26
|
+
if ! File.directory?(path)
|
27
|
+
puts "Error: The home directory is invalid: the subdirectory #{subdir} doesn't exist under the home directory. Was the setup command run?"
|
28
|
+
rc = false
|
29
|
+
break
|
30
|
+
end
|
31
|
+
end
|
32
|
+
rc
|
33
|
+
end
|
34
|
+
|
35
|
+
def setup
|
36
|
+
@subdirs.each do |subdir|
|
37
|
+
if File.directory?(subdir)
|
38
|
+
puts "Directory #{subdir} already exists. Skipping creation."
|
39
|
+
else
|
40
|
+
installedPath = @installRoot + File::SEPARATOR + subdir
|
41
|
+
if File.directory? installedPath
|
42
|
+
FileUtils.cp_r installedPath, @dir
|
43
|
+
puts "Copying #{subdir}"
|
44
|
+
else
|
45
|
+
FileUtils.mkdir @dir + File::SEPARATOR + subdir
|
46
|
+
puts "Creating #{subdir}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Create database
|
52
|
+
dbFile = "#{File.expand_path(@dir)}/db/quartz.sqlite"
|
53
|
+
if ! File.exists?(dbFile)
|
54
|
+
puts "Creating database file"
|
55
|
+
path = "sqlite://#{File.expand_path(@dir)}/db/quartz.sqlite"
|
56
|
+
DataMapper.setup(:default, path)
|
57
|
+
DataMapper.auto_migrate!
|
58
|
+
else
|
59
|
+
puts "Database file already exists. Running upgrade."
|
60
|
+
path = "sqlite://#{File.expand_path(@dir)}/db/quartz.sqlite"
|
61
|
+
DataMapper.setup(:default, path)
|
62
|
+
DataMapper.auto_upgrade!
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.determineAppRoot(gemname)
|
68
|
+
# Are we running as a Gem, or from the source directory?
|
69
|
+
if Gem.loaded_specs[gemname]
|
70
|
+
Gem.loaded_specs[gemname].full_gem_path
|
71
|
+
else
|
72
|
+
"."
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
@@ -0,0 +1,247 @@
|
|
1
|
+
require 'quartz_torrent/magnet'
|
2
|
+
|
3
|
+
module BEncode
|
4
|
+
class DecodeError
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
|
9
|
+
module QuartzTorrent
|
10
|
+
class Words
|
11
|
+
def initialize
|
12
|
+
@words = []
|
13
|
+
File.open "/usr/share/dict/words", "r" do |file|
|
14
|
+
file.each_line do |l|
|
15
|
+
@words.push l.chomp
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def randomWord
|
21
|
+
@words[rand(@words.size)]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Metainfo
|
26
|
+
|
27
|
+
def self.createFromFile(file)
|
28
|
+
end
|
29
|
+
|
30
|
+
class FileInfo
|
31
|
+
def initialize(length = nil, path = nil)
|
32
|
+
@length = length
|
33
|
+
@path = path
|
34
|
+
end
|
35
|
+
|
36
|
+
# Relative path to the file. For a single-file torrent this is simply the name of the file. For a multi-file torrent,
|
37
|
+
# this is the directory names from the torrent and the filename separated by the file separator.
|
38
|
+
attr_accessor :path
|
39
|
+
# Length of the file.
|
40
|
+
attr_accessor :length
|
41
|
+
end
|
42
|
+
|
43
|
+
class Info
|
44
|
+
def initialize
|
45
|
+
@files = []
|
46
|
+
@name = nil
|
47
|
+
@pieceLen = 0
|
48
|
+
@pieces = []
|
49
|
+
@private = false
|
50
|
+
end
|
51
|
+
|
52
|
+
# Array of FileInfo objects
|
53
|
+
attr_accessor :files
|
54
|
+
# Suggested file or directory name
|
55
|
+
attr_accessor :name
|
56
|
+
# Length of each piece in bytes. The last piece may be shorter than this.
|
57
|
+
attr_accessor :pieceLen
|
58
|
+
# Array of SHA1 digests of all peices. These digests are in binary format.
|
59
|
+
attr_accessor :pieces
|
60
|
+
# True if no external peer source is allowed.
|
61
|
+
attr_accessor :private
|
62
|
+
|
63
|
+
# Total length of the torrent data in bytes.
|
64
|
+
def dataLength
|
65
|
+
files.reduce(0){ |memo,f| memo + f.length}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class TrackerPeer
|
71
|
+
def initialize(ip, port, id = nil)
|
72
|
+
@ip = ip
|
73
|
+
@port = port
|
74
|
+
@id = id
|
75
|
+
end
|
76
|
+
|
77
|
+
attr_accessor :ip
|
78
|
+
attr_accessor :port
|
79
|
+
attr_accessor :id
|
80
|
+
end
|
81
|
+
|
82
|
+
class Peer
|
83
|
+
def initialize(trackerPeer)
|
84
|
+
@trackerPeer = trackerPeer
|
85
|
+
@amChoked = true
|
86
|
+
@amInterested = false
|
87
|
+
@peerChoked = true
|
88
|
+
@peerInterested = false
|
89
|
+
@infoHash = nil
|
90
|
+
@state = :disconnected
|
91
|
+
@uploadRate = 0
|
92
|
+
@downloadRate = 0
|
93
|
+
@uploadRateDataOnly = 0
|
94
|
+
@downloadRateDataOnly = 0
|
95
|
+
@bitfield = nil
|
96
|
+
@firstEstablishTime = nil
|
97
|
+
@isUs = false
|
98
|
+
@requestedBlocks = {}
|
99
|
+
@requestedBlocksSizeLastPass = nil
|
100
|
+
@maxRequestedBlocks = 50
|
101
|
+
end
|
102
|
+
|
103
|
+
attr_accessor :trackerPeer
|
104
|
+
attr_accessor :amChoked
|
105
|
+
attr_accessor :amInterested
|
106
|
+
attr_accessor :peerChoked
|
107
|
+
attr_accessor :peerInterested
|
108
|
+
attr_accessor :infoHash
|
109
|
+
attr_accessor :firstEstablishTime
|
110
|
+
attr_accessor :maxRequestedBlocks
|
111
|
+
attr_accessor :state
|
112
|
+
attr_accessor :isUs
|
113
|
+
attr_accessor :uploadRate
|
114
|
+
attr_accessor :downloadRate
|
115
|
+
attr_accessor :uploadRateDataOnly
|
116
|
+
attr_accessor :downloadRateDataOnly
|
117
|
+
attr_accessor :bitfield
|
118
|
+
attr_accessor :requestedBlocks
|
119
|
+
attr_accessor :requestedBlocksSizeLastPass
|
120
|
+
attr_accessor :peerMsgSerializer
|
121
|
+
end
|
122
|
+
|
123
|
+
class TorrentDataDelegate
|
124
|
+
# Create a new TorrentDataDelegate. This is meant to only be called internally.
|
125
|
+
def initialize
|
126
|
+
@info = nil
|
127
|
+
@infoHash = nil
|
128
|
+
@recommendedName = nil
|
129
|
+
@downloadRate = nil
|
130
|
+
@uploadRate = nil
|
131
|
+
@downloadRateDataOnly = nil
|
132
|
+
@uploadRateDataOnly = nil
|
133
|
+
@completedBytes = nil
|
134
|
+
@peers = nil
|
135
|
+
@state = nil
|
136
|
+
@completePieceBitfield = nil
|
137
|
+
@metainfoLength = nil
|
138
|
+
@metainfoCompletedLength = nil
|
139
|
+
@paused = nil
|
140
|
+
end
|
141
|
+
|
142
|
+
# Torrent Metainfo.info struct. This is nil if the torrent has no metadata and we haven't downloaded it yet
|
143
|
+
# (i.e. a magnet link).
|
144
|
+
attr_accessor :info
|
145
|
+
attr_accessor :infoHash
|
146
|
+
# Recommended display name for this torrent.
|
147
|
+
attr_accessor :recommendedName
|
148
|
+
attr_accessor :downloadRate
|
149
|
+
attr_accessor :uploadRate
|
150
|
+
attr_accessor :downloadRateDataOnly
|
151
|
+
attr_accessor :uploadRateDataOnly
|
152
|
+
attr_accessor :completedBytes
|
153
|
+
attr_accessor :peers
|
154
|
+
# State of the torrent. This may be one of :downloading_metainfo, :error, :checking_pieces, :running, :downloading_metainfo, or :deleted.
|
155
|
+
# The :deleted state indicates that the torrent that this TorrentDataDelegate refers to is no longer being managed by the peer client.
|
156
|
+
attr_accessor :state
|
157
|
+
attr_accessor :completePieceBitfield
|
158
|
+
# Length of metainfo info in bytes. This is only set when the state is :downloading_metainfo
|
159
|
+
attr_accessor :metainfoLength
|
160
|
+
# How much of the metainfo info we have downloaded in bytes. This is only set when the state is :downloading_metainfo
|
161
|
+
attr_accessor :metainfoCompletedLength
|
162
|
+
attr_accessor :paused
|
163
|
+
end
|
164
|
+
|
165
|
+
class PeerClient
|
166
|
+
def initialize(ignored)
|
167
|
+
@words = Words.new
|
168
|
+
@torrents = {}
|
169
|
+
7.times{ addTorrent }
|
170
|
+
Thread.new do
|
171
|
+
while true
|
172
|
+
begin
|
173
|
+
@torrents.each{ |k,t| t.downloadRate += (rand(1000) - 500)}
|
174
|
+
rescue
|
175
|
+
puts "Exception in Mock PeerClient: #{$!}"
|
176
|
+
end
|
177
|
+
sleep 2
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def port=(p)
|
183
|
+
end
|
184
|
+
|
185
|
+
def start
|
186
|
+
end
|
187
|
+
|
188
|
+
def stop
|
189
|
+
end
|
190
|
+
|
191
|
+
def addTorrentByMetainfo(info)
|
192
|
+
end
|
193
|
+
|
194
|
+
def torrentData(infoHash = nil)
|
195
|
+
@torrents
|
196
|
+
end
|
197
|
+
|
198
|
+
def addTorrent
|
199
|
+
d = makeFakeTorrentDelegate
|
200
|
+
@torrents[d.infoHash] = d
|
201
|
+
end
|
202
|
+
|
203
|
+
private
|
204
|
+
def makeFakeTorrentDelegate
|
205
|
+
result = TorrentDataDelegate.new
|
206
|
+
name = ""
|
207
|
+
(rand(3)+1).times do
|
208
|
+
name << @words.randomWord + " "
|
209
|
+
end
|
210
|
+
result.recommendedName = name
|
211
|
+
result.infoHash = randomBinaryData 20
|
212
|
+
result.downloadRate = 100*1024
|
213
|
+
result.uploadRate = 5*1024
|
214
|
+
result.downloadRateDataOnly = 100*1024
|
215
|
+
result.uploadRateDataOnly = 5*1024
|
216
|
+
result.completedBytes = 800
|
217
|
+
result.peers = []
|
218
|
+
result.state = :running
|
219
|
+
result.paused = false
|
220
|
+
|
221
|
+
result.info = Metainfo::Info.new
|
222
|
+
result.info.files = [Metainfo::FileInfo.new(1000, "file1"), Metainfo::FileInfo.new(1000, "file2")]
|
223
|
+
|
224
|
+
3.times do
|
225
|
+
|
226
|
+
peer = Peer.new(TrackerPeer.new("176.23.54.201", 5000))
|
227
|
+
peer.amChoked = false
|
228
|
+
peer.amInterested = false
|
229
|
+
peer.peerChoked = false
|
230
|
+
peer.peerInterested = true
|
231
|
+
peer.uploadRate = 20*1024
|
232
|
+
peer.downloadRate = 10*1024
|
233
|
+
|
234
|
+
result.peers.push peer
|
235
|
+
end
|
236
|
+
|
237
|
+
result
|
238
|
+
end
|
239
|
+
|
240
|
+
def randomBinaryData(len)
|
241
|
+
result = ""
|
242
|
+
len.times{ result << rand(256) }
|
243
|
+
result
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'data_mapper'
|
2
|
+
|
3
|
+
class Setting
|
4
|
+
include DataMapper::Resource
|
5
|
+
|
6
|
+
property :id, Serial
|
7
|
+
property :name, String
|
8
|
+
property :value, String
|
9
|
+
property :scope, Enum[:global, :user, :torrent]
|
10
|
+
# For settings that are not global, owner identifies who they apply to.
|
11
|
+
# For user settings, this is the user. For torrent settings this is the torrent infohash in hex ascii
|
12
|
+
property :owner, String
|
13
|
+
end
|
14
|
+
|
15
|
+
class UsageBucket
|
16
|
+
include DataMapper::Resource
|
17
|
+
|
18
|
+
property :id, Serial
|
19
|
+
property :type, Enum[:daily, :monthly]
|
20
|
+
property :index, Integer
|
21
|
+
property :label, String
|
22
|
+
property :criteriaData, Time
|
23
|
+
property :absoluteUsage, Integer
|
24
|
+
property :value, Integer
|
25
|
+
end
|
26
|
+
|
27
|
+
DataMapper.finalize
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class RandString
|
2
|
+
SALT_CHARS = %w{a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 1 2 3 4 5 6 7 8 9 0 _ % $ @ ! " ' . , < > }
|
3
|
+
|
4
|
+
def self.make_random_string(len)
|
5
|
+
rc = ""
|
6
|
+
len.times{ rc << SALT_CHARS[rand(SALT_CHARS.size)] }
|
7
|
+
rc
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|