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
@@ -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
|
+
|