quartz_flow 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,247 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'quartz_torrent.rb'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'quartz_flow/usagetracker'
|
5
|
+
require 'thread'
|
6
|
+
|
7
|
+
class TorrentManager
|
8
|
+
def initialize(peerClient, torrentFileDir, monthlyResetDay)
|
9
|
+
@peerClient = peerClient
|
10
|
+
@cachedTorrentData = nil
|
11
|
+
@cachedAt = nil
|
12
|
+
@cacheLifetime = 2
|
13
|
+
@torrentFileDir = torrentFileDir
|
14
|
+
@peerClientStopped = false
|
15
|
+
@usageTracker = UsageTracker.new(monthlyResetDay)
|
16
|
+
# Start a thread to keep track of usage.
|
17
|
+
startUsageTrackerThread
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :peerClient
|
21
|
+
|
22
|
+
def torrentData(infoHash = nil)
|
23
|
+
if (! @cachedTorrentData || Time.new - @cachedAt > @cacheLifetime) && ! @peerClientStopped
|
24
|
+
@cachedTorrentData = @peerClient.torrentData
|
25
|
+
@cachedAt = Time.new
|
26
|
+
end
|
27
|
+
|
28
|
+
@cachedTorrentData
|
29
|
+
end
|
30
|
+
|
31
|
+
def stopPeerClient
|
32
|
+
@peerClient.stop
|
33
|
+
@peerClientStopped = true
|
34
|
+
end
|
35
|
+
|
36
|
+
# Start torrents that already exist in the torrent file directory
|
37
|
+
def startExistingTorrents
|
38
|
+
Dir.new(@torrentFileDir).each do |e|
|
39
|
+
path = @torrentFileDir + File::SEPARATOR + e
|
40
|
+
if e =~ /\.torrent$/
|
41
|
+
puts "Starting .torrent '#{path}'"
|
42
|
+
startTorrentFile(path)
|
43
|
+
elsif e =~ /\.magnet$/
|
44
|
+
magnet = loadMagnet(path)
|
45
|
+
puts "Starting magnet '#{magnet.raw}'"
|
46
|
+
startMagnet magnet
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Convert torrent data such that:
|
52
|
+
# - The TorrentDataDelegate objects are converted to hashes.
|
53
|
+
def simplifiedTorrentData
|
54
|
+
result = {}
|
55
|
+
torrentData.each do |k,d|
|
56
|
+
h = d.to_h
|
57
|
+
asciiInfoHash = QuartzTorrent::bytesToHex(h[:infoHash])
|
58
|
+
h[:infoHash] = asciiInfoHash
|
59
|
+
h[:downloadRate] = QuartzTorrent::Formatter.formatSpeed(h[:downloadRate])
|
60
|
+
h[:uploadRate] = QuartzTorrent::Formatter.formatSpeed(h[:uploadRate])
|
61
|
+
h[:downloadRateDataOnly] = QuartzTorrent::Formatter.formatSpeed(h[:downloadRateDataOnly])
|
62
|
+
h[:uploadRateDataOnly] = QuartzTorrent::Formatter.formatSpeed(h[:uploadRateDataOnly])
|
63
|
+
h[:dataLength] = QuartzTorrent::Formatter.formatSize(h[:dataLength])
|
64
|
+
h[:completedBytes] = QuartzTorrent::Formatter.formatSize(h[:completedBytes])
|
65
|
+
# Sort peers
|
66
|
+
h[:peers].sort! do |a,b|
|
67
|
+
c = (b[:uploadRate].to_i <=> a[:uploadRate].to_i)
|
68
|
+
c = (b[:downloadRate].to_i <=> a[:downloadRate].to_i) if c == 0
|
69
|
+
c
|
70
|
+
end
|
71
|
+
# Format peer rates
|
72
|
+
h[:peers].each do |p|
|
73
|
+
p[:uploadRate] = QuartzTorrent::Formatter.formatSpeed(p[:uploadRate])
|
74
|
+
p[:downloadRate] = QuartzTorrent::Formatter.formatSpeed(p[:downloadRate])
|
75
|
+
end
|
76
|
+
if h[:info]
|
77
|
+
h[:info][:files].each do |file|
|
78
|
+
file[:length] = QuartzTorrent::Formatter.formatSize(file[:length])
|
79
|
+
end
|
80
|
+
end
|
81
|
+
h[:uploadRateLimit] = QuartzTorrent::Formatter.formatSpeed(h[:uploadRateLimit])
|
82
|
+
h[:downloadRateLimit] = QuartzTorrent::Formatter.formatSize(h[:downloadRateLimit])
|
83
|
+
h[:bytesUploaded] = QuartzTorrent::Formatter.formatSize(h[:bytesUploaded])
|
84
|
+
h[:bytesDownloaded] = QuartzTorrent::Formatter.formatSize(h[:bytesDownloaded])
|
85
|
+
|
86
|
+
h[:completePieces] = d.completePieceBitfield ? d.completePieceBitfield.countSet : 0
|
87
|
+
h[:totalPieces] = d.completePieceBitfield ? d.completePieceBitfield.length : 0
|
88
|
+
|
89
|
+
result[asciiInfoHash] = h
|
90
|
+
end
|
91
|
+
result
|
92
|
+
end
|
93
|
+
|
94
|
+
# Download a .torrent file from a specified URL. Return the path to the
|
95
|
+
# downloaded .torrent file.
|
96
|
+
def downloadTorrentFile(url)
|
97
|
+
# open-uri doesn't handle [ and ] properly
|
98
|
+
encodedSourcePath = URI.escape(url, /[\[\]]/)
|
99
|
+
|
100
|
+
path = nil
|
101
|
+
open(encodedSourcePath) do |f|
|
102
|
+
uriPath = f.base_uri.path
|
103
|
+
raise "The file '#{uriPath}' doesn't have the .torrent extension" if uriPath !~ /.torrent$/
|
104
|
+
path = @torrentFileDir + File::SEPARATOR + File.basename(uriPath)
|
105
|
+
File.open(path, "w"){ |outfile| outfile.write(f.read) }
|
106
|
+
end
|
107
|
+
path
|
108
|
+
end
|
109
|
+
|
110
|
+
# Store a magnet link in a file in the torrent file directory.
|
111
|
+
def storeMagnet(magnet)
|
112
|
+
asciiInfoHash = QuartzTorrent::bytesToHex(magnet.btInfoHash)
|
113
|
+
path = @torrentFileDir + File::SEPARATOR + asciiInfoHash + ".magnet"
|
114
|
+
File.open(path, "w"){ |outfile| outfile.write(magnet.raw) }
|
115
|
+
end
|
116
|
+
|
117
|
+
# Load a magnet link in a file
|
118
|
+
def loadMagnet(path)
|
119
|
+
raw = nil
|
120
|
+
File.open(path, "r"){ |infile| raw = infile.read }
|
121
|
+
QuartzTorrent::MagnetURI.new(raw)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Store an uploaded .torrent file in the torrent directory. Return the path to the
|
125
|
+
# final .torrent file.
|
126
|
+
def storeUploadedTorrentFile(path, name)
|
127
|
+
name += ".torrent" if name !~ /\.torrent$/
|
128
|
+
dpath = @torrentFileDir + File::SEPARATOR + name
|
129
|
+
FileUtils.mv path, dpath
|
130
|
+
dpath
|
131
|
+
end
|
132
|
+
|
133
|
+
# Start running the torrent specified by the .torrent file given in path.
|
134
|
+
def startTorrentFile(path)
|
135
|
+
startTorrent do
|
136
|
+
begin
|
137
|
+
metainfo = QuartzTorrent::Metainfo.createFromFile(path)
|
138
|
+
@peerClient.addTorrentByMetainfo(metainfo)
|
139
|
+
rescue BEncode::DecodeError
|
140
|
+
# Delete the file
|
141
|
+
begin
|
142
|
+
FileUtils.rm path
|
143
|
+
rescue
|
144
|
+
end
|
145
|
+
raise $!
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Start running the magnet
|
151
|
+
def startMagnet(magnet)
|
152
|
+
startTorrent do
|
153
|
+
@peerClient.addTorrentByMagnetURI(magnet)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Remove the specified torrent. Pass the infoHash as an ascii string, not binary.
|
158
|
+
def removeTorrent(infoHash, deleteFiles)
|
159
|
+
infoHashBytes = QuartzTorrent::hexToBytes(infoHash)
|
160
|
+
@peerClient.removeTorrent infoHashBytes, deleteFiles
|
161
|
+
|
162
|
+
# Remove torrent from torrent dir
|
163
|
+
Dir.new(@torrentFileDir).each do |e|
|
164
|
+
if e =~ /\.torrent$/
|
165
|
+
path = @torrentFileDir + File::SEPARATOR + e
|
166
|
+
metainfo = QuartzTorrent::Metainfo.createFromFile(path)
|
167
|
+
if metainfo.infoHash == infoHashBytes
|
168
|
+
FileUtils.rm path
|
169
|
+
break
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Remove torrent settings
|
175
|
+
helper = SettingsHelper.new
|
176
|
+
helper.deleteForOwner infoHash
|
177
|
+
|
178
|
+
# Remove magnet file if it exists
|
179
|
+
magnetFile = @torrentFileDir + File::SEPARATOR + infoHash + ".magnet"
|
180
|
+
FileUtils.rm magnetFile if File.exists?(magnetFile)
|
181
|
+
end
|
182
|
+
|
183
|
+
# Update the torrent settings (upload rate limit, etc) from database values
|
184
|
+
def applyTorrentSettings(infoHash)
|
185
|
+
asciiInfoHash = QuartzTorrent::bytesToHex(infoHash)
|
186
|
+
helper = SettingsHelper.new
|
187
|
+
|
188
|
+
# Set limits based on per-torrent settings if they exist, otherwise to default global limits if they exist.
|
189
|
+
uploadRateLimit = to_i(helper.get(:uploadRateLimit, :unfiltered, asciiInfoHash))
|
190
|
+
uploadRateLimit = to_i(helper.get(:defaultUploadRateLimit, :unfiltered)) if ! uploadRateLimit
|
191
|
+
|
192
|
+
downloadRateLimit = to_i(helper.get(:downloadRateLimit, :unfiltered, asciiInfoHash))
|
193
|
+
downloadRateLimit = to_i(helper.get(:defaultDownloadRateLimit, :unfiltered)) if ! downloadRateLimit
|
194
|
+
|
195
|
+
ratio = helper.get(:ratio, :filter, asciiInfoHash)
|
196
|
+
ratio = helper.get(:defaultRatio, :filter) if ! ratio
|
197
|
+
|
198
|
+
@peerClient.setUploadRateLimit infoHash, uploadRateLimit
|
199
|
+
@peerClient.setDownloadRateLimit infoHash, downloadRateLimit
|
200
|
+
@peerClient.setUploadRatio infoHash, ratio
|
201
|
+
end
|
202
|
+
|
203
|
+
# Get the usage for the current period of the specified type.
|
204
|
+
# periodType should be one of :daily or :monthly.
|
205
|
+
def currentPeriodUsage(periodType)
|
206
|
+
@usageTracker.currentUsage(periodType).value
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
# Helper for starting torrents. Expects a block that when called will add a torrent to the
|
211
|
+
# @peerClient, and return the infoHash.
|
212
|
+
def startTorrent
|
213
|
+
raise "Torrent client is shutting down" if @peerClientStopped
|
214
|
+
infoHash = yield
|
215
|
+
|
216
|
+
applyTorrentSettings infoHash
|
217
|
+
end
|
218
|
+
|
219
|
+
def to_i(val)
|
220
|
+
val = val.to_i if val
|
221
|
+
val
|
222
|
+
end
|
223
|
+
|
224
|
+
# Start a thread to keep track of usage.
|
225
|
+
def startUsageTrackerThread
|
226
|
+
@usageTrackerThread = Thread.new do
|
227
|
+
QuartzTorrent.initThread("torrent_usage_tracking")
|
228
|
+
|
229
|
+
Thread.current[:stopped] = false
|
230
|
+
|
231
|
+
while ! Thread.current[:stopped]
|
232
|
+
begin
|
233
|
+
sleep 4
|
234
|
+
torrentData = @peerClient.torrentData
|
235
|
+
usage = 0
|
236
|
+
torrentData.each do |k,v|
|
237
|
+
usage += v.bytesUploaded + v.bytesDownloaded
|
238
|
+
end
|
239
|
+
@usageTracker.update(usage)
|
240
|
+
rescue
|
241
|
+
puts "Error in usage tracking thread: #{$!}"
|
242
|
+
puts $!.backtrace.join "\n"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
@@ -0,0 +1,335 @@
|
|
1
|
+
require 'quartz_flow/model'
|
2
|
+
|
3
|
+
class Bucket
|
4
|
+
def initialize(label = nil, criteriaData = nil, value = nil)
|
5
|
+
@label = label
|
6
|
+
@criteriaData = criteriaData
|
7
|
+
@value = value
|
8
|
+
@absoluteUsageAtStartOfBucket = nil
|
9
|
+
end
|
10
|
+
attr_accessor :label
|
11
|
+
# Data used by BucketChangeCriteria to determine when we need a new bucket.
|
12
|
+
attr_accessor :criteriaData
|
13
|
+
# At the time this bucket was created, the value of the absolute usage for all time
|
14
|
+
attr_accessor :absoluteUsageAtStartOfBucket
|
15
|
+
# The amount of usage for this bucket alone
|
16
|
+
attr_accessor :value
|
17
|
+
|
18
|
+
def toHash
|
19
|
+
{"label" => @label, "absoluteUsageAtStartOfBucket" => @absoluteUsageAtStartOfBucket, "criteriaData" => @criteriaData, "value" => @value}
|
20
|
+
end
|
21
|
+
|
22
|
+
def fromHash(hash)
|
23
|
+
@label = hash["label"]
|
24
|
+
@absoluteUsageAtStartOfBucket = hash["absoluteUsageAtStartOfBucket"]
|
25
|
+
@criteriaData = hash["criteriaData"]
|
26
|
+
@value = hash["value"]
|
27
|
+
end
|
28
|
+
|
29
|
+
def fromModel(bucket)
|
30
|
+
@label = bucket.label
|
31
|
+
@absoluteUsageAtStartOfBucket = bucket.absoluteUsage
|
32
|
+
@criteriaData = bucket.criteriaData
|
33
|
+
@value = bucket.value
|
34
|
+
end
|
35
|
+
|
36
|
+
def toModel
|
37
|
+
model = UsageBucket.new
|
38
|
+
model.attributes = {
|
39
|
+
:label => @label,
|
40
|
+
:absoluteUsage => @absoluteUsageAtStartOfBucket,
|
41
|
+
:criteriaData => @criteriaData,
|
42
|
+
:value => @value
|
43
|
+
}
|
44
|
+
model
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class BucketChangeCriteria
|
49
|
+
# Is it now time for a new bucket?
|
50
|
+
def newBucket?(currentBucket)
|
51
|
+
false
|
52
|
+
end
|
53
|
+
|
54
|
+
# Make a new bucket and return it.
|
55
|
+
def newBucket
|
56
|
+
innerNewBucket
|
57
|
+
end
|
58
|
+
|
59
|
+
def innerNewBucket
|
60
|
+
raise "Implement me!"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class PeriodicBuckets
|
65
|
+
def initialize(criteria, maxBuckets = nil)
|
66
|
+
setBucketChangeCriteria(criteria)
|
67
|
+
@buckets = []
|
68
|
+
@maxBuckets = maxBuckets
|
69
|
+
@maxBuckets = 1 if @maxBuckets && @maxBuckets < 1
|
70
|
+
end
|
71
|
+
|
72
|
+
# Set the criteria that determines when the current bucket is full
|
73
|
+
# and we should make a new empty bucket the current bucket, and that is used
|
74
|
+
# to set the label for the new bucket.
|
75
|
+
def setBucketChangeCriteria(criteria)
|
76
|
+
@bucketChangeCriteria = criteria
|
77
|
+
end
|
78
|
+
|
79
|
+
def update(absoluteUsage = nil)
|
80
|
+
if @buckets.size == 0
|
81
|
+
prev = nil
|
82
|
+
@buckets.push @bucketChangeCriteria.newBucket
|
83
|
+
setAbsoluteUsage(prev, @buckets.last, absoluteUsage) if absoluteUsage
|
84
|
+
else
|
85
|
+
prev = @buckets.last
|
86
|
+
# Time for a new bucket?
|
87
|
+
if @bucketChangeCriteria.newBucket?(@buckets.last)
|
88
|
+
@buckets.push @bucketChangeCriteria.newBucket
|
89
|
+
setAbsoluteUsage(prev, @buckets.last, absoluteUsage) if absoluteUsage
|
90
|
+
end
|
91
|
+
@buckets.shift if @maxBuckets && @buckets.size > @maxBuckets
|
92
|
+
end
|
93
|
+
|
94
|
+
setValue(@buckets.last, absoluteUsage) if absoluteUsage
|
95
|
+
end
|
96
|
+
|
97
|
+
def current(absoluteUsage = nil)
|
98
|
+
@buckets.last
|
99
|
+
end
|
100
|
+
|
101
|
+
def all
|
102
|
+
@buckets
|
103
|
+
end
|
104
|
+
|
105
|
+
def toHash
|
106
|
+
array = []
|
107
|
+
@buckets.each do |b|
|
108
|
+
array.push b.toHash
|
109
|
+
end
|
110
|
+
{ "buckets" => array }
|
111
|
+
end
|
112
|
+
|
113
|
+
def fromHash(hash)
|
114
|
+
@buckets = []
|
115
|
+
hash["buckets"].each do |b|
|
116
|
+
bucket = Bucket.new(nil, nil, nil)
|
117
|
+
bucket.fromHash b
|
118
|
+
@buckets.push bucket
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def fromModel(type)
|
123
|
+
@buckets = []
|
124
|
+
buckets = UsageBucket.all(:type => type, :order => [:index.asc])
|
125
|
+
buckets.each do |model|
|
126
|
+
bucket = Bucket.new(nil, nil, nil)
|
127
|
+
bucket.fromModel model
|
128
|
+
@buckets.push bucket
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def toModel(type)
|
133
|
+
UsageBucket.all(:type => type).destroy!
|
134
|
+
index = 0
|
135
|
+
@buckets.each do |b|
|
136
|
+
model = b.toModel
|
137
|
+
model.type = type
|
138
|
+
model.index = index
|
139
|
+
index += 1
|
140
|
+
model.save
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
def setAbsoluteUsage(previousBucket, newBucket, absoluteUsage)
|
146
|
+
if previousBucket
|
147
|
+
newBucket.absoluteUsageAtStartOfBucket = previousBucket.absoluteUsageAtStartOfBucket + previousBucket.value
|
148
|
+
else
|
149
|
+
newBucket.absoluteUsageAtStartOfBucket = absoluteUsage
|
150
|
+
end
|
151
|
+
end
|
152
|
+
def setValue(newBucket, absoluteUsage)
|
153
|
+
newBucket.value =
|
154
|
+
absoluteUsage -
|
155
|
+
newBucket.absoluteUsageAtStartOfBucket
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
class DailyBucketChangeCriteria < BucketChangeCriteria
|
160
|
+
def newBucket?(currentBucket)
|
161
|
+
now = Time.new
|
162
|
+
currentBucket.criteriaData.day != now.day
|
163
|
+
end
|
164
|
+
|
165
|
+
def newBucket
|
166
|
+
now = Time.new
|
167
|
+
Bucket.new(now.strftime("%b %e"), now, 0)
|
168
|
+
end
|
169
|
+
|
170
|
+
def criteriaData
|
171
|
+
Time.new
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
class MonthlyBucketChangeCriteria < BucketChangeCriteria
|
176
|
+
def initialize(resetDay)
|
177
|
+
@resetDay = resetDay
|
178
|
+
end
|
179
|
+
|
180
|
+
def newBucket?(currentBucket)
|
181
|
+
Time.new > currentBucket.criteriaData
|
182
|
+
end
|
183
|
+
|
184
|
+
def newBucket
|
185
|
+
now = Time.new
|
186
|
+
# Set the bucket's criteriaData to the date after which we need a new bucket.
|
187
|
+
data = criteriaData
|
188
|
+
Bucket.new(now.strftime("%b %Y"), data, 0)
|
189
|
+
end
|
190
|
+
|
191
|
+
def criteriaData
|
192
|
+
now = Time.new
|
193
|
+
nextMonth = now.mon % 12 + 1
|
194
|
+
Time.local(now.year, nextMonth, @resetDay)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
|
199
|
+
# For testing
|
200
|
+
class MinuteBucketChangeCriteria < BucketChangeCriteria
|
201
|
+
def newBucket?(currentBucket)
|
202
|
+
now = Time.new
|
203
|
+
currentBucket.criteriaData.min != now.min
|
204
|
+
end
|
205
|
+
|
206
|
+
def newBucket
|
207
|
+
now = Time.new
|
208
|
+
Bucket.new(now.strftime("%H:%M"), now, 0)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
class UsageTracker
|
213
|
+
def initialize(monthlyResetDay)
|
214
|
+
@buckets = {}
|
215
|
+
@buckets[:daily] = PeriodicBuckets.new(DailyBucketChangeCriteria.new,31)
|
216
|
+
#@buckets[:minute] = PeriodicBuckets.new(MinuteBucketChangeCriteria.new,3)
|
217
|
+
@buckets[:monthly] = PeriodicBuckets.new(MonthlyBucketChangeCriteria.new(monthlyResetDay),2)
|
218
|
+
@usageForAllTimeAdjustment = 0
|
219
|
+
loadBucketsFromDatastore
|
220
|
+
end
|
221
|
+
|
222
|
+
# Update the UsageTracker with more usage. The value passed
|
223
|
+
# should be the usage since the torrentflow session was created.
|
224
|
+
# If a datastore is not used, then this means stopping and starting the session
|
225
|
+
# will cause UsageTracking to only track usage for the session. However
|
226
|
+
# if Mongo is used then the usage can be saved and persisted between sessions
|
227
|
+
# and internally the value passed here is added to the value loaded from Mongo.
|
228
|
+
def update(usageForAllTime)
|
229
|
+
usageForAllTime += @usageForAllTimeAdjustment
|
230
|
+
@buckets.each do |k,buckets|
|
231
|
+
buckets.update usageForAllTime
|
232
|
+
end
|
233
|
+
saveBucketsToDatastore
|
234
|
+
end
|
235
|
+
|
236
|
+
# This method returns the usage in the current bucket for the specified
|
237
|
+
# period type (:daily or :monthly). The usage is accurate as of the last
|
238
|
+
# time update() was called.
|
239
|
+
# The returned value is a single Bucket object.
|
240
|
+
def currentUsage(periodType)
|
241
|
+
getBuckets(periodType).current
|
242
|
+
end
|
243
|
+
|
244
|
+
# Returns the usage as of the last time update() was called.
|
245
|
+
# This method returns all the tracked usage for the specified
|
246
|
+
# period type (:daily or :monthly). The usage is accurate as of the last
|
247
|
+
# time update() was called.
|
248
|
+
# The returned value is an array of Bucket objects.
|
249
|
+
def allUsage(periodType)
|
250
|
+
getBuckets(periodType).all
|
251
|
+
end
|
252
|
+
|
253
|
+
private
|
254
|
+
def getBuckets(type)
|
255
|
+
buckets = @buckets[type]
|
256
|
+
raise "Unsupported periodType #{periodType.to_s}" if ! buckets
|
257
|
+
buckets
|
258
|
+
end
|
259
|
+
|
260
|
+
def saveBucketsToDatastore
|
261
|
+
@buckets[:daily].toModel(:daily)
|
262
|
+
@buckets[:monthly].toModel(:monthly)
|
263
|
+
=begin
|
264
|
+
if @mongoDb
|
265
|
+
dailyCollection = @mongoDb.collection("daily_usage")
|
266
|
+
monthlyCollection = @mongoDb.collection("monthly_usage")
|
267
|
+
# Remove all previous documents
|
268
|
+
dailyCollection.remove
|
269
|
+
monthlyCollection.remove
|
270
|
+
|
271
|
+
dailyCollection.insert @buckets[:daily].toHash
|
272
|
+
monthlyCollection.insert @buckets[:monthly].toHash
|
273
|
+
end
|
274
|
+
=end
|
275
|
+
end
|
276
|
+
|
277
|
+
def loadBucketsFromDatastore
|
278
|
+
@buckets[:daily].fromModel(:daily)
|
279
|
+
@buckets[:monthly].fromModel(:monthly)
|
280
|
+
|
281
|
+
# If we are loading from datastore it means that the absolute usage returned from the current torrent session will not
|
282
|
+
# contain the usage that we previously tracked, so we must add the old tracked value to what the torrent
|
283
|
+
# session reports.
|
284
|
+
if @buckets[:daily].current
|
285
|
+
@usageForAllTimeAdjustment = @buckets[:daily].current.absoluteUsageAtStartOfBucket + @buckets[:daily].current.value
|
286
|
+
end
|
287
|
+
|
288
|
+
=begin
|
289
|
+
if @mongoDb
|
290
|
+
$logger.info "Loading usage from Mongo."
|
291
|
+
dailyCollection = @mongoDb.collection("daily_usage")
|
292
|
+
monthlyCollection = @mongoDb.collection("monthly_usage")
|
293
|
+
|
294
|
+
arr = dailyCollection.find_one
|
295
|
+
@buckets[:daily].fromHash arr if arr
|
296
|
+
arr = monthlyCollection.find_one
|
297
|
+
@buckets[:monthly].fromHash arr if arr
|
298
|
+
|
299
|
+
# If we are loading from Mongo it means that the absolute usage returned from the torrentflow session will not
|
300
|
+
# contain the usage that we previously tracked, so we must add the old tracked value to what the torrentflow
|
301
|
+
# session reports.
|
302
|
+
if @buckets[:daily].current
|
303
|
+
@usageForAllTimeAdjustment = @buckets[:daily].current.absoluteUsageAtStartOfBucket + @buckets[:daily].current.value
|
304
|
+
$logger.info "Absolute usage at start of current daily bucket: " + @buckets[:daily].current.absoluteUsageAtStartOfBucket.to_s
|
305
|
+
$logger.info "Usage in current daily bucket: " + @buckets[:daily].current.value.to_s
|
306
|
+
$logger.info "Usage for all time adjustment: " + @usageForAllTimeAdjustment.to_s
|
307
|
+
else
|
308
|
+
$logger.info "No usage loaded in Mongo (empty collection)."
|
309
|
+
end
|
310
|
+
else
|
311
|
+
$logger.info "Not loading usage from Mongo."
|
312
|
+
end
|
313
|
+
=end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
=begin
|
318
|
+
# Testing
|
319
|
+
tracker = UsageTracker.new
|
320
|
+
|
321
|
+
abs = 200
|
322
|
+
while true
|
323
|
+
tracker.update(abs)
|
324
|
+
|
325
|
+
puts
|
326
|
+
puts "Usage for all time: #{abs}"
|
327
|
+
puts "Buckets:"
|
328
|
+
tracker.allUsage(:minute).each do |b|
|
329
|
+
puts " #{b.label}: #{b.value}"
|
330
|
+
end
|
331
|
+
abs += 10
|
332
|
+
sleep 10
|
333
|
+
end
|
334
|
+
|
335
|
+
=end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'quartz_torrent/formatter.rb'
|
2
|
+
|
3
|
+
module QuartzTorrent
|
4
|
+
|
5
|
+
class Metainfo::FileInfo
|
6
|
+
def to_h
|
7
|
+
{ path: @path, length: @length }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Metainfo::Info
|
12
|
+
def to_h
|
13
|
+
result = {}
|
14
|
+
|
15
|
+
result[:name] = @name
|
16
|
+
result[:pieceLen] = @pieceLen
|
17
|
+
result[:files] = @files.collect{ |e| e.to_h }
|
18
|
+
|
19
|
+
result
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class TrackerPeer
|
24
|
+
def to_h
|
25
|
+
{ ip: @ip, port: @port }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Peer
|
30
|
+
def to_h
|
31
|
+
result = {}
|
32
|
+
|
33
|
+
result[:trackerPeer] = @trackerPeer.to_h
|
34
|
+
result[:amChoked] = @amChoked
|
35
|
+
result[:amInterested] = @amInterested
|
36
|
+
result[:peerChoked] = @peerChoked
|
37
|
+
result[:peerInterested] = @peerInterested
|
38
|
+
result[:firstEstablishTime] = @firstEstablishTime
|
39
|
+
result[:maxRequestedBlocks] = @maxRequestedBlocks
|
40
|
+
result[:state] = @state
|
41
|
+
result[:isUs] = @isUs
|
42
|
+
result[:uploadRate] = @uploadRate
|
43
|
+
result[:downloadRate] = @downloadRate
|
44
|
+
if @bitfield
|
45
|
+
result[:pctComplete] = "%.2f" % (100.0 * @bitfield.countSet / @bitfield.length)
|
46
|
+
else
|
47
|
+
result[:pctComplete] = 0
|
48
|
+
end
|
49
|
+
|
50
|
+
result
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class TorrentDataDelegate
|
55
|
+
# Convert to a hash. Also flattens some of the data into new fields.
|
56
|
+
def to_h
|
57
|
+
result = {}
|
58
|
+
|
59
|
+
## Extra fields added by this method:
|
60
|
+
# Length of the torrent
|
61
|
+
result[:dataLength] = @info ? @info.dataLength : 0
|
62
|
+
# Percent complete
|
63
|
+
pct = withCurrentAndTotalBytes{ |cur, total| (cur.to_f / total.to_f * 100.0).round 1 }
|
64
|
+
result[:percentComplete] = pct
|
65
|
+
# Time left
|
66
|
+
secondsLeft = withCurrentAndTotalBytes do |cur, total|
|
67
|
+
if @downloadRateDataOnly && @downloadRateDataOnly > 0
|
68
|
+
(total.to_f - cur.to_f) / @downloadRateDataOnly
|
69
|
+
else
|
70
|
+
0
|
71
|
+
end
|
72
|
+
end
|
73
|
+
result[:timeLeft] = Formatter.formatTime(secondsLeft)
|
74
|
+
|
75
|
+
## Regular fields
|
76
|
+
result[:info] = @info ? @info.to_h : nil
|
77
|
+
result[:infoHash] = @infoHash
|
78
|
+
result[:recommendedName] = @recommendedName
|
79
|
+
result[:downloadRate] = @downloadRate
|
80
|
+
result[:uploadRate] = @uploadRate
|
81
|
+
result[:downloadRateDataOnly] = @downloadRateDataOnly
|
82
|
+
result[:uploadRateDataOnly] = @uploadRateDataOnly
|
83
|
+
result[:completedBytes] = @completedBytes
|
84
|
+
result[:peers] = @peers.collect{ |p| p.to_h }
|
85
|
+
result[:state] = @state
|
86
|
+
#result[:completePieceBitfield] = @completePieceBitfield
|
87
|
+
result[:metainfoLength] = @metainfoLength
|
88
|
+
result[:metainfoCompletedLength] = @metainfoCompletedLength
|
89
|
+
result[:paused] = @paused
|
90
|
+
result[:uploadRateLimit] = @uploadRateLimit
|
91
|
+
result[:downloadRateLimit] = @downloadRateLimit
|
92
|
+
result[:ratio] = @ratio
|
93
|
+
result[:bytesUploaded] = @bytesUploaded
|
94
|
+
result[:bytesDownloaded] = @bytesDownloaded
|
95
|
+
|
96
|
+
result
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
def withCurrentAndTotalBytes
|
101
|
+
if @info
|
102
|
+
yield @completedBytes, @info.dataLength
|
103
|
+
elsif @state == :downloading_metainfo && @metainfoCompletedLength && @metainfoLength
|
104
|
+
yield @metainfoCompletedLength, @metainfoLength
|
105
|
+
else
|
106
|
+
0
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
def calcEstimatedTime(torrent)
|
112
|
+
# Time left = amount_left / download_rate
|
113
|
+
# = total_size * (1-progress) / download_rate
|
114
|
+
if torrentHandle.has_metadata && torrentHandle.status.download_rate.to_f > 0
|
115
|
+
secondsLeft = torrentHandle.info.total_size.to_f * (1 - torrentHandle.status.progress.to_f) / torrentHandle.status.download_rate.to_f
|
116
|
+
Formatter.formatTime(secondsLeft)
|
117
|
+
else
|
118
|
+
"unknown"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|