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.
@@ -0,0 +1,305 @@
1
+ # Register an at_exit handler so that when Sinatra is shutting down
2
+ # we stop the torrent peer client.
3
+ $manager = nil
4
+ at_exit do
5
+ puts "Stopping torrent client"
6
+ $manager.stopPeerClient if $manager
7
+ end
8
+
9
+ require 'haml'
10
+ require 'json'
11
+ require 'quartz_torrent'
12
+ require 'quartz_flow/wrappers'
13
+ require 'quartz_flow/torrent_manager'
14
+ require 'quartz_flow/settings_helper'
15
+ require 'quartz_flow/authentication'
16
+ require 'quartz_flow/session'
17
+ require 'fileutils'
18
+ require 'sinatra/base'
19
+
20
+ class LogConfigurator
21
+ def self.set(logger, level)
22
+ QuartzTorrent::LogManager.setLevel logger, level
23
+ end
24
+
25
+ def self.configLevels
26
+ # Load configuration settings
27
+ path = "./etc/logging.rb"
28
+ return if ! File.exists?(path)
29
+ eval File.open(path,"r").read
30
+ end
31
+ end
32
+
33
+ class Server < Sinatra::Base
34
+ configure do
35
+ enable :sessions
36
+ set :bind, '0.0.0.0'
37
+ set :port, 4444
38
+ set :basedir, "download"
39
+ set :torrent_port, 9996
40
+ set :db_file, "db/quartz.sqlite"
41
+ set :torrent_log, "log/torrent.log"
42
+ set :password_file, "etc/passwd"
43
+ set :logging, true
44
+ set :monthly_usage_reset_day, 1
45
+
46
+ # Load configuration settings
47
+ eval File.open("./etc/quartz.rb","r").read
48
+
49
+ set :root, '.'
50
+
51
+ raise "The basedir '#{settings.basedir}' does not exist. Please create it." if ! File.directory? settings.basedir
52
+ raise "The metadir '#{settings.metadir}' does not exist. Please create it." if ! File.directory? settings.metadir
53
+
54
+ QuartzTorrent::LogManager.initializeFromEnv
55
+ logfile = settings.torrent_log
56
+ QuartzTorrent::LogManager.setup do
57
+ setLogfile logfile
58
+ setDefaultLevel :info
59
+ end
60
+ LogConfigurator.configLevels
61
+ peerClient = QuartzTorrent::PeerClient.new(settings.basedir)
62
+ peerClient.port = settings.torrent_port
63
+ peerClient.start
64
+
65
+ # Initialize Datamapper
66
+ path = "sqlite://#{Dir.pwd}/#{settings.db_file}"
67
+ DataMapper.setup(:default, path)
68
+
69
+ $manager = TorrentManager.new(peerClient, settings.metadir, settings.monthly_usage_reset_day)
70
+ $manager.startExistingTorrents
71
+ end
72
+
73
+ before do
74
+ if $useAuthentication
75
+ sid = session[:sid]
76
+ if ! SessionStore.instance.valid_session?(sid)
77
+ if request.path_info == "/"
78
+ # Redirect to login if not authenticated
79
+ session[:redir] = request.path_info
80
+ request.path_info = "/login"
81
+ elsif request.path_info != "/login"
82
+ halt 500, "Authentication required"
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ get "/login" do
89
+ haml :login
90
+ end
91
+
92
+ post "/login" do
93
+ json = JSON.parse(request.body.read)
94
+ halt 500, "Missing login" if ! json['login']
95
+ halt 500, "Missing password" if ! json['password']
96
+
97
+ auth = Authentication.new settings.password_file
98
+ if auth.authenticate json['login'], json['password']
99
+ sid = SessionStore.instance.start_session(params[:login].to_s)
100
+ session[:sid] = sid
101
+ else
102
+ halt 500, "Invalid login or password"
103
+ end
104
+ "OK"
105
+ end
106
+
107
+ post "/logout" do
108
+ SessionStore.instance.end_session(session[:sid])
109
+ session.delete :sid
110
+ "OK"
111
+ end
112
+
113
+ get "/" do
114
+ haml :index
115
+ end
116
+
117
+ # Get the HTML template used by the Angular module
118
+ # to display the table of running torrents view.
119
+ get "/torrent_table" do
120
+ haml :torrent_table_partial
121
+ end
122
+
123
+ # Get the HTML template used by the Angular module
124
+ # to display the details of a single running torrent view.
125
+ get "/torrent_detail" do
126
+ haml :torrent_detail_partial
127
+ end
128
+
129
+
130
+ # Get the HTML template used by the Angular module
131
+ # to display the config settings
132
+ get "/config" do
133
+ haml :config_partial
134
+ end
135
+
136
+ # Get an array of JSON objects that represent a list of current running
137
+ # torrents with various properties.
138
+ get "/torrent_data" do
139
+ JSON.generate $manager.simplifiedTorrentData
140
+ end
141
+
142
+ # Get usage as a JSON object.
143
+ get "/usage" do
144
+ hash = {
145
+ :monthlyUsage => QuartzTorrent::Formatter.formatSize($manager.currentPeriodUsage(:monthly)),
146
+ :dailyUsage => QuartzTorrent::Formatter.formatSize($manager.currentPeriodUsage(:daily))
147
+ }
148
+ JSON.generate hash
149
+ end
150
+
151
+ # Download a .torrent file and start running it.
152
+ # The body of the post should be JSON encoded.
153
+ post "/download_torrent" do
154
+ json = JSON.parse(request.body.read)
155
+
156
+ url = json["url"]
157
+ halt 500, "Downloading torrent file failed: no url parameter was sent to the server in the post request." if ! url || url.length == 0
158
+ path = nil
159
+ begin
160
+ path = $manager.downloadTorrentFile url
161
+ rescue
162
+ halt 500, "Downloading torrent file failed: #{$!}"
163
+ end
164
+
165
+ begin
166
+ $manager.startTorrentFile(path)
167
+ rescue BEncode::DecodeError
168
+ halt 500, "Starting torrent file failed: torrent file is malformed."
169
+ rescue
170
+ puts $!
171
+ puts $!.backtrace.join("\n")
172
+ halt 500, "Starting torrent file failed: #{$!}."
173
+ end
174
+
175
+ # We need to return something here otherwise AngularJS chokes.
176
+ "Worked fine"
177
+ end
178
+
179
+ # Given a magnet link, start running it.
180
+ # The body of the post should be JSON encoded.
181
+ post "/start_magnet" do
182
+ json = JSON.parse(request.body.read)
183
+
184
+ url = json["url"]
185
+ halt 500, "Starting magnet link failed: no url parameter was sent to the server in the post request." if ! url || url.length == 0
186
+ halt 500, "Starting magnet link failed: the link doesn't appear to be a magnet link" if ! QuartzTorrent::MagnetURI.magnetURI?(url)
187
+
188
+ magnet = QuartzTorrent::MagnetURI.new(url)
189
+ begin
190
+ $manager.storeMagnet(magnet)
191
+ rescue
192
+ halt 500, "Storing magnet link failed: #{$!}."
193
+ end
194
+
195
+ begin
196
+ $manager.startMagnet(magnet)
197
+ rescue
198
+ puts $!
199
+ puts $!.backtrace.join("\n")
200
+ halt 500, "Starting magnet link failed: #{$!}."
201
+ end
202
+
203
+ # We need to return something here otherwise AngularJS chokes.
204
+ "Worked fine"
205
+ end
206
+
207
+ # Handle an upload of a torrent file.
208
+ post "/upload_torrent" do
209
+ # See http://www.wooptoot.com/file-upload-with-sinatra
210
+ path = params['torrentfile'][:tempfile].path
211
+
212
+ #FileUtils.chmod 0644, path
213
+ path = $manager.storeUploadedTorrentFile path, params['torrentfile'][:filename]
214
+
215
+ begin
216
+ $manager.startTorrentFile(path)
217
+ rescue BEncode::DecodeError
218
+ halt 500, "Starting torrent file failed: torrent file is malformed."
219
+ rescue
220
+ puts $!
221
+ puts $!.backtrace.join("\n")
222
+ halt 500, "Starting torrent file failed: #{$!}."
223
+ end
224
+
225
+ # We need to return something here otherwise AngularJS chokes.
226
+ "@@success"
227
+ end
228
+
229
+ post "/pause_torrent" do
230
+ json = JSON.parse(request.body.read)
231
+
232
+ infoHash = json["infohash"]
233
+ halt 500, "Pausing torrent failed: no infohash parameter was sent to the server in the post request." if !infoHash || infoHash.length == 0
234
+ $manager.peerClient.setPaused QuartzTorrent::hexToBytes(infoHash), true
235
+ puts "Pausing torrent"
236
+ "OK"
237
+ end
238
+
239
+ post "/unpause_torrent" do
240
+ json = JSON.parse(request.body.read)
241
+
242
+ infoHash = json["infohash"]
243
+ halt 500, "Unpausing torrent failed: no infohash parameter was sent to the server in the post request." if !infoHash || infoHash.length == 0
244
+ $manager.peerClient.setPaused QuartzTorrent::hexToBytes(infoHash), false
245
+ "OK"
246
+ end
247
+
248
+ post "/delete_torrent" do
249
+ json = JSON.parse(request.body.read)
250
+
251
+ infoHash = json["infohash"]
252
+ halt 500, "Deleting torrent failed: no infohash parameter was sent to the server in the post request." if !infoHash || infoHash.length == 0
253
+ deleteFiles = json["delete_files"]
254
+ halt 500, "Deleting torrent failed: no delete_files parameter was sent to the server in the post request." if deleteFiles.nil?
255
+
256
+ begin
257
+ $manager.removeTorrent infoHash, deleteFiles
258
+ rescue
259
+ halt 500, "Removing torrent failed: #{$!}"
260
+ end
261
+
262
+ "OK"
263
+ end
264
+
265
+ # Return all UI configurable settings from the model as
266
+ # a hash.
267
+ get "/global_settings" do
268
+ settings = SettingsHelper.new.globalSettingsHash
269
+ JSON.generate settings
270
+ end
271
+
272
+ post "/global_settings" do
273
+ helper = SettingsHelper.new
274
+ json = JSON.parse(request.body.read)
275
+ begin
276
+ helper.setGlobalSettingsHash(json)
277
+ rescue
278
+ halt 500, "Saving global settings failed: #{$!}"
279
+ end
280
+ "OK"
281
+ end
282
+
283
+ post "/change_torrent" do
284
+ helper = SettingsHelper.new
285
+ json = JSON.parse(request.body.read)
286
+
287
+ asciiInfoHash = json['infoHash']
288
+ halt 500, "Saving torrent settings failed: no infoHash parameter was sent to the server in the post request." if ! asciiInfoHash
289
+
290
+ json.each do |k,v|
291
+ next if k == 'infoHash'
292
+ begin
293
+ helper.set k, v, asciiInfoHash
294
+ rescue
295
+ halt 500, "Saving torrent settings failed: #{$!}"
296
+ end
297
+ end
298
+
299
+ infoHash = QuartzTorrent::hexToBytes(asciiInfoHash)
300
+ $manager.applyTorrentSettings infoHash
301
+
302
+ "OK"
303
+ end
304
+
305
+ end
@@ -0,0 +1,83 @@
1
+ require 'thread'
2
+ require 'singleton'
3
+ require 'quartz_flow/randstring'
4
+
5
+ class Session
6
+ def initialize(sid = nil, login = nil, length = 60*60)
7
+ @sid = sid
8
+ @login = login
9
+ # Make a 1 hr session
10
+ @expiry = Time.new + length
11
+ end
12
+ attr_accessor :sid
13
+ attr_accessor :login
14
+ attr_accessor :expiry
15
+
16
+ def expired?
17
+ @expiry < Time.new
18
+ end
19
+ end
20
+
21
+ # Use SessionStore.instance to access the singleton.
22
+ class SessionStore
23
+ include Singleton
24
+
25
+ def initialize
26
+ @sessions = {}
27
+ @session_mutex = Mutex.new
28
+ @audit_thread = Thread.new do
29
+ while true
30
+ begin
31
+ sleep 10
32
+ audit_sessions
33
+ rescue
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ # Start a new session for the specified user.
40
+ def start_session(login)
41
+ sid = nil
42
+ while ! sid || @sessions.has_key?(sid)
43
+ sid = RandString.make_random_string(256)
44
+ end
45
+ @session_mutex.synchronize do
46
+ @sessions[sid] = Session.new(sid, login)
47
+ end
48
+ sid
49
+ end
50
+
51
+ def end_session(sid)
52
+ return if ! sid
53
+ @session_mutex.synchronize do
54
+ @sessions.delete sid
55
+ end
56
+ end
57
+
58
+ def valid_session?(sid)
59
+ rc = false
60
+ return rc if ! sid
61
+ @session_mutex.synchronize do
62
+ session = @sessions[sid]
63
+ if session != nil
64
+ if !session.expired?
65
+ rc = true
66
+ else
67
+ @sessions.delete sid
68
+ end
69
+ end
70
+ end
71
+ rc
72
+ end
73
+
74
+ def audit_sessions
75
+ @sessions.each do |k,session|
76
+ if session.expired?
77
+ @session_mutex.synchronize do
78
+ @sessions.delete k
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,154 @@
1
+ require 'quartz_flow/model'
2
+ require 'quartz_torrent/formatter'
3
+
4
+ class SettingsHelper
5
+ class SettingMetainfo
6
+ def initialize(name, scope, saveFilter = nil, loadFilter = nil, emptyIsNil = true)
7
+ @name = name
8
+ @scope = scope
9
+ @saveFilter = saveFilter
10
+ @loadFilter = loadFilter
11
+ @emptyIsNil = emptyIsNil
12
+ end
13
+ attr_accessor :name
14
+ attr_accessor :scope
15
+ # If the value is empty, treat it as a nil value when writing to database
16
+ def emptyIsNil?
17
+ @emptyIsNil
18
+ end
19
+ def filterOnSave(v)
20
+ filter @saveFilter, v
21
+ end
22
+ def filterOnLoad(v)
23
+ filter @loadFilter, v
24
+ end
25
+ private
26
+ def filter(afilter, v)
27
+ if afilter && v
28
+ afilter.call(v)
29
+ else
30
+ v
31
+ end
32
+ end
33
+ end
34
+
35
+ @@floatValidator = Proc.new do |v|
36
+ raise "Invalid ratio" if v !~ /^\d+(\.\d+)?$/
37
+ v.to_s
38
+ end
39
+
40
+ @@saveFilterForSize = Proc.new do |v|
41
+ if v.nil? || v.length == 0
42
+ nil
43
+ else
44
+ QuartzTorrent::Formatter.parseSize(v)
45
+ end
46
+ end
47
+
48
+
49
+ @@settingsMetainfo = {
50
+ :defaultUploadRateLimit => SettingMetainfo.new(
51
+ :defaultUploadRateLimit,
52
+ :global,
53
+ @@saveFilterForSize,
54
+ Proc.new{ |v| QuartzTorrent::Formatter.formatSpeed(v) }
55
+ ),
56
+ :defaultDownloadRateLimit => SettingMetainfo.new(
57
+ :defaultDownloadRateLimit,
58
+ :global,
59
+ @@saveFilterForSize,
60
+ Proc.new{ |v| QuartzTorrent::Formatter.formatSpeed(v) }
61
+ ),
62
+ :defaultRatio => SettingMetainfo.new(
63
+ :defaultRatio,
64
+ :global,
65
+ @@floatValidator,
66
+ Proc.new{ |v| v.to_f }
67
+ ),
68
+ :uploadRateLimit => SettingMetainfo.new(
69
+ :uploadRateLimit,
70
+ :torrent,
71
+ @@saveFilterForSize,
72
+ Proc.new{ |v| QuartzTorrent::Formatter.formatSpeed(v) }
73
+ ),
74
+ :downloadRateLimit => SettingMetainfo.new(
75
+ :downloadRateLimit,
76
+ :torrent,
77
+ @@saveFilterForSize,
78
+ Proc.new{ |v| QuartzTorrent::Formatter.formatSpeed(v) }
79
+ ),
80
+ :ratio => SettingMetainfo.new(
81
+ :ratio,
82
+ :torrent,
83
+ @@floatValidator,
84
+ Proc.new{ |v| v.to_f }
85
+ ),
86
+ }
87
+
88
+ def set(settingName, value, owner = nil)
89
+ setting = settingName.to_sym
90
+
91
+ metaInfo = @@settingsMetainfo[setting]
92
+ raise "Unknown setting #{settingName}" if ! metaInfo
93
+
94
+ value = nil if metaInfo.emptyIsNil? && value.is_a?(String) && value.length == 0
95
+ value = value.to_s if value
96
+ value = metaInfo.filterOnSave(value)
97
+
98
+ settingModel = loadWithOwner(settingName, owner)
99
+
100
+ if ! settingModel
101
+ Setting.create( :name => settingName, :value => value, :scope => metaInfo.scope, :owner => owner )
102
+ else
103
+ settingModel.value = value
104
+ settingModel.save
105
+ end
106
+ end
107
+
108
+ def get(settingName, filter = :filter, owner = nil)
109
+ setting = settingName.to_sym
110
+ metaInfo = @@settingsMetainfo[setting]
111
+ raise "Unknown setting #{settingName}" if ! metaInfo
112
+
113
+ result = nil
114
+ settingModel = loadWithOwner(settingName, owner)
115
+
116
+ if settingModel
117
+ result = settingModel.value
118
+ result = metaInfo.filterOnLoad(result) if filter == :filter
119
+ end
120
+ result
121
+ end
122
+
123
+ def deleteForOwner(owner)
124
+ Setting.all(:owner => owner).destroy!
125
+ end
126
+
127
+ # Return a hashtable of all global settings
128
+ def globalSettingsHash
129
+ result = {}
130
+
131
+ @@settingsMetainfo.each do |k,v|
132
+ next if v.scope != :global
133
+ result[k] = get(k)
134
+ end
135
+
136
+ result
137
+ end
138
+
139
+ # Set global settings as a hash
140
+ def setGlobalSettingsHash(hash)
141
+ @@settingsMetainfo.each do |k,v|
142
+ set(k, hash[k.to_s]) if hash.has_key?(k.to_s)
143
+ end
144
+ end
145
+
146
+ private
147
+ def loadWithOwner(settingName, owner)
148
+ if owner
149
+ Setting.first(:name => settingName, :owner => owner)
150
+ else
151
+ Setting.first(:name => settingName)
152
+ end
153
+ end
154
+ end