jeeves-pvr 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,209 @@
1
+ #
2
+ #
3
+ # = Scheduler
4
+ #
5
+ # == Base class
6
+ #
7
+ # Author:: Robert Sharp
8
+ # Copyright:: Copyright (c) 2011 Robert Sharp
9
+ # License:: Open Software Licence v3.0
10
+ #
11
+ # This software is licensed for use under the Open Software Licence v. 3.0
12
+ # The terms of this licence can be found at http://www.opensource.org/licenses/osl-3.0.php
13
+ # and in the file copyright.txt. Under the terms of this licence, all derivative works
14
+ # must themselves be licensed under the Open Software Licence v. 3.0
15
+ #
16
+ # A base class for the Jeeves Tuner and Scheduler
17
+ #
18
+ # This class needs to be inherited to create specific subclasses for different tuners
19
+ #
20
+
21
+ require 'jerbil/jerbil_service/base'
22
+ require 'jerbil/jerbil_service/support'
23
+ require 'jerbil/support'
24
+
25
+ require 'jeeves/config'
26
+ require 'jeeves/errors'
27
+
28
+ require 'open3'
29
+
30
+ #
31
+ # == Jeeves Service
32
+ #
33
+ # Synopsis of the whole service
34
+ #
35
+ module Jscheduler
36
+
37
+ # add generic class methods
38
+ extend JerbilService::Support
39
+
40
+ # inherit the Jeeves config file
41
+ class Config < Jeeves::Config; end
42
+
43
+ # === Jeeves Service class
44
+ #
45
+ # Description of how the service class works
46
+ #
47
+ class Base < JerbilService::Base
48
+
49
+ # Document the constructor as required.
50
+ #
51
+ # pkey:: string - private key that must be used for
52
+ # supervision calls (e.g. stop_callback)
53
+ # options:: hash of options, preferably as created by Jeckyl. See documentation
54
+ # on JerbilService::Base for assumed options re logging etc
55
+ #
56
+ # Note that this method should be super'd before any local initialization
57
+ #
58
+ def initialize(pkey, options)
59
+
60
+ @tuner = options[:tuner]
61
+ @device = '/dev/video0' # set this to the device from which the tuner streams
62
+
63
+ # do things that cannot be done when $SAFE > 0
64
+
65
+ # the symbol should be the app name and must correspond to a service in /etc/services
66
+ super(:jschedule, pkey, options)
67
+
68
+ # DRb is now working, this process may have daemonized and $SAFE = 1
69
+
70
+ # the current priority, an integer where the higher the number the higher the priority
71
+ @priority = 0
72
+
73
+ # save the pid for the tuner process if there is one
74
+ @tuner_pid = nil
75
+
76
+ @logger.verbose("Started scheduler for tuner #{@tuner}")
77
+
78
+ end
79
+
80
+ #
81
+ # define additional class methods to make things happen
82
+ #
83
+
84
+ # tune_receiver is needed for each receiver to prepare that receiver for
85
+ # a given channel.
86
+ #
87
+ # calls init_receiver to prepare the receiver itself
88
+ # and then sets a timer to protect the tuner
89
+ def tune_receiver(priority, params={})
90
+
91
+ # do not tune anything if there is a higher priority tune in progress
92
+ raise Jeeves::TunerBusy if @priority > 0 && priority <= @priority
93
+
94
+ # do the specific stuff for the given tuner and get back its pid (or not if there
95
+ # is not process involved)
96
+ self.stop_tuner(@tuner_pid) unless @tuner_pid.nil?
97
+
98
+ @tuner_pid = self.init_tuner(params)
99
+
100
+ if params[:duration] then
101
+ # need to do this for a given time
102
+ @priority = priority
103
+ @thread = Thread.new do
104
+ # use Jeckyl to ensure this is sensible!
105
+ sleep(params[:duration])
106
+ # now clear up
107
+ @priority = 0
108
+ self.stop_tuner(@tuner_pid)
109
+ end
110
+ else
111
+ # no duration, so tune indefinitely
112
+ # BUT what if there is a priority? Ignore it!
113
+ @priority = 0
114
+ end
115
+ #raise NotImplementedError
116
+ # needs to do what ever the tuner requires to tune to the given channel
117
+ # For Drb tuners this may involve a process running continuously, which
118
+ # can be ended either after a certain time or when another call is made
119
+ # to tune the receiver.
120
+
121
+ # params include:
122
+ #
123
+ # * an identity for the channel as required by the tuner
124
+ # * a priority - to allow recordings to overide casual viewing, for example
125
+ # * a duration that enforces the priority and may also hold the tuner on.
126
+ end
127
+
128
+ # this will record the current receiver settings to a given file. It needs
129
+ # to know the device from which to record and will then use FFMPEG to do
130
+ # the recording
131
+ def record(filename)
132
+
133
+ end
134
+
135
+ # play the current tuned channel on the primary X display
136
+ def play
137
+
138
+ end
139
+
140
+ # stream the current tuned channel over the net using netcat
141
+ def stream
142
+
143
+ end
144
+
145
+ # this will create an at job to record a programme at some time in the future
146
+ def schedule(params={})
147
+ [:filename, :channel, :start, :duration].each do |param|
148
+ raise Jeeves::MissingParam, "Schedule method must specify #{param}" unless params.has_key?(param)
149
+ end
150
+ mode = params[:mode] || :avi
151
+ raise InvalidMode, "Tuner #{@tuner} does not have mode: #{mode}" unless Schedulers[@tuner].has_key?(mode)
152
+ scheduler = Schedulers[@tuner][mode]
153
+
154
+ startParam = params[:start].strftime("%H:%M %b %d")
155
+
156
+ command = "#{scheduler} #{params[:channel]} #{params[:duration]} #{params[:filename]}"
157
+
158
+ @logger.debug "About to schedule: #{command}"
159
+
160
+ # need open3 to get the full communication with at
161
+ inpipe, outpipe, errpipe = Open3.popen3("/usr/bin/at #{startParam}")
162
+ inpipe.puts command
163
+ inpipe.close
164
+ response = errpipe.readlines
165
+ outpipe.close
166
+ errpipe.close
167
+
168
+ # get the job id and return it to the caller
169
+ jobnum = nil
170
+ job_re = /job (\d+) at/
171
+ response.each do |resp|
172
+ @logger.debug "Response includes: " + resp
173
+ if results = job_re.match(resp) then
174
+ @logger.debug "Recognised Job: " + results[1]
175
+ jobnum = results[1].clone
176
+ end
177
+ end
178
+
179
+ @logger.info "Scheduled recording on #{params[:channel]} at #{startParam} for #{params[:duration]} with job no: #{jobnum}"
180
+ # set a wol timer here
181
+
182
+ return jobnum
183
+ rescue Jeeves::JeevesError
184
+ raise
185
+ rescue
186
+ @logger.exception($!)
187
+ return '0'
188
+ end
189
+
190
+
191
+ # deletes the at job with the given job number
192
+ # assume that the given jobnum is valid
193
+ def delete_schedule(jobnum)
194
+ begin
195
+ @logger.info "Removing job: #{jobnum}"
196
+ system("/usr/bin/atrm #{jobnum.to_s}")
197
+ rescue
198
+ @logger.exception($!)
199
+ end
200
+ end
201
+
202
+ # do not redefine the superclass methods unless you are absolutely sure you know
203
+ # what you are doing.
204
+
205
+ protected
206
+
207
+
208
+ end
209
+ end
@@ -0,0 +1,148 @@
1
+ #
2
+ #
3
+ # = Jeeves Scheduler
4
+ #
5
+ # == Schedule Jeeves related tasks from recordings to shutdowns and wakeups
6
+ #
7
+ # Author:: Robert Sharp
8
+ # Copyright:: Copyright (c) 2011 Robert Sharp
9
+ # License:: Open Software Licence v3.0
10
+ #
11
+ # This software is licensed for use under the Open Software Licence v. 3.0
12
+ # The terms of this licence can be found at http://www.opensource.org/licenses/osl-3.0.php
13
+ # and in the file copyright.txt. Under the terms of this licence, all derivative works
14
+ # must themselves be licensed under the Open Software Licence v. 3.0
15
+ #
16
+ # This scheduler will schedule at jobs and use wolly to schedule wakeups. It is a Jerbil
17
+ # service.
18
+ #
19
+
20
+ require 'jerbil/jerbil_service/base'
21
+ require 'jerbil/jerbil_service/support'
22
+ require 'jerbil/support'
23
+
24
+ require 'jeeves/config'
25
+ require 'jeeves/errors'
26
+
27
+ require 'open3'
28
+
29
+ #
30
+ # == Jeeves Service
31
+ #
32
+ # Synopsis of the whole service
33
+ #
34
+ module Jscheduler
35
+
36
+ # add generic class methods
37
+ extend JerbilService::Support
38
+
39
+ # inherit the Jeeves config file
40
+ class Config < Jeeves::Config; end
41
+
42
+ # === Jeeves Service class
43
+ #
44
+ # Description of how the service class works
45
+ #
46
+ class Service < JerbilService::Base
47
+
48
+ # Document the constructor as required.
49
+ #
50
+ # pkey:: string - private key that must be used for
51
+ # supervision calls (e.g. stop_callback)
52
+ # options:: hash of options, preferably as created by Jeckyl. See documentation
53
+ # on JerbilService::Base for assumed options re logging etc
54
+ #
55
+ def initialize(pkey, options)
56
+
57
+ @tuner = options[:tuner]
58
+ raise InvalidTuner("Tuner is not recognized: #{@tuner}") unless Schedulers.has_key?(@tuner)
59
+
60
+ # do things that cannot be done when $SAFE > 0
61
+
62
+ # the symbol should be the app name and must correspond to a service in /etc/services
63
+ super(:jschedule, pkey, options)
64
+
65
+ # DRb is now working, this process may have daemonized and $SAFE = 1
66
+
67
+ @logger.verbose("Started scheduler for tuner #{@tuner}")
68
+
69
+ end
70
+
71
+ #
72
+ # define additional class methods to make things happen
73
+ #
74
+
75
+ def schedule(params={})
76
+ [:filename, :channel, :start, :duration].each do |param|
77
+ raise Jeeves::MissingParam, "Schedule method must specify #{param}" unless params.has_key?(param)
78
+ end
79
+ mode = params[:mode] || :avi
80
+ raise InvalidMode, "Tuner #{@tuner} does not have mode: #{mode}" unless Schedulers[@tuner].has_key?(mode)
81
+ scheduler = Schedulers[@tuner][mode]
82
+
83
+ startParam = params[:start].strftime("%H:%M %b %d")
84
+
85
+ command = "#{scheduler} #{params[:channel]} #{params[:duration]} #{params[:filename]}"
86
+
87
+ @logger.debug "About to schedule: #{command}"
88
+
89
+ # need open3 to get the full communication with at
90
+ inpipe, outpipe, errpipe = Open3.popen3("/usr/bin/at #{startParam}")
91
+ inpipe.puts command
92
+ inpipe.close
93
+ response = errpipe.readlines
94
+ outpipe.close
95
+ errpipe.close
96
+
97
+ # get the job id and return it to the caller
98
+ jobnum = nil
99
+ job_re = /job (\d+) at/
100
+ response.each do |resp|
101
+ @logger.debug "Response includes: " + resp
102
+ if results = job_re.match(resp) then
103
+ @logger.debug "Recognised Job: " + results[1]
104
+ jobnum = results[1].clone
105
+ end
106
+ end
107
+
108
+ @logger.info "Scheduled recording on #{params[:channel]} at #{startParam} for #{params[:duration]} with job no: #{jobnum}"
109
+ # set a wol timer here
110
+
111
+ return jobnum
112
+ rescue Jeeves::JeevesError
113
+ raise
114
+ rescue
115
+ @logger.exception($!)
116
+ return '0'
117
+ end
118
+
119
+
120
+ # deletes the at job with the given job number
121
+ # assume that the given jobnum is valid
122
+ def delete_schedule(jobnum)
123
+ begin
124
+ @logger.info "Removing job: #{jobnum}"
125
+ system("/usr/bin/atrm #{jobnum.to_s}")
126
+ rescue
127
+ @logger.exception($!)
128
+ end
129
+ end
130
+
131
+ # do not redefine the superclass methods unless you are absolutely sure you know
132
+ # what you are doing.
133
+
134
+ protected
135
+
136
+ Schedulers = {
137
+ :dvb=>{
138
+ :avi=>'/usr/local/bin/virea-dvb-record.sh',
139
+ :ts=>'/usr/local/bin/virea-dvb-record-ts.sh'
140
+ },
141
+ :pvr=>{
142
+ :avi=>'/usr/local/bin/virea-pvr-record.sh',
143
+ :ts=>'/usr/local/bin/virea-pvr-record-ts.sh'
144
+ }
145
+ }
146
+
147
+ end
148
+ end
@@ -0,0 +1,544 @@
1
+ #
2
+ # Author:: R.J.Sharp
3
+ # Email:: robert(a)osburn-sharp.ath.cx
4
+ # Copyright:: Copyright (c) 2013
5
+ # License:: Open Software Licence v3.0
6
+ #
7
+ # This software is licensed for use under the Open Software Licence v. 3.0
8
+ # The terms of this licence can be found at http://www.opensource.org/licenses/osl-3.0.php
9
+ # and in the file LICENCE. Under the terms of this licence, all derivative works
10
+ # must themselves be licensed under the Open Software Licence v. 3.0
11
+ #
12
+ #
13
+
14
+ require 'jerbil/jerbil_service/support'
15
+
16
+
17
+ require 'jellog'
18
+
19
+ require 'yaml'
20
+ require 'open-uri'
21
+ require 'digest'
22
+
23
+ require 'jeeves/partition'
24
+ require 'jeeves/errors'
25
+ require 'jeeves/version'
26
+ require 'jeeves/utils'
27
+
28
+
29
+ module Jeeves
30
+
31
+ # Check if this is really needed...
32
+ #extend JerbilService::Support
33
+
34
+ # === Jeeves Store class
35
+ #
36
+ # Store manages the allocation of video files for use by Jeeves. Multiple
37
+ # locations can be added to a store and Jestore distributes the video files
38
+ # across these in proportion to their size. Files are pre-allocated when a
39
+ # recording is scheduled and then released when the recording completes.
40
+ # Store also notifies Jeeves when a video is released.
41
+ #
42
+ # NOTE: Store assumes it has all of the space on a given partition. For example,
43
+ # if you define two stores that are actually on the same partition then Store
44
+ # will report twice the space that is actually available! Probably needs fixing to
45
+ # detect duplicate partitions at some point.
46
+ #
47
+ # Store is not an active daemon but just an interface. It uses file locking to
48
+ # avoid any conflicts with multiple versions running across the network.
49
+ #
50
+ class Store
51
+
52
+ # create an interface to Store.
53
+ #
54
+ # creates lookup tables but does not fill them at this stage
55
+ #
56
+ # @param [Jeckyl::Config] options hash as defined in {Jeeves:Config}
57
+ def initialize(logger, options)
58
+
59
+ # do things that cannot be done when $SAFE > 0
60
+
61
+ @store_params = options[:add_partition]
62
+ @store_errors = options[:store_errors]
63
+ @partitions = Array.new
64
+ @logger = logger
65
+ @file_list_path = nil
66
+ # lookup of device to free space
67
+ @space_lup = Hash.new
68
+ # lookup of path to device
69
+ @dev_lup = Hash.new
70
+
71
+ check_partitions
72
+
73
+ @base_rate = options[:base_data_rate]
74
+ @jeeves_url = options[:jeeves_url]
75
+
76
+ # hash of allocated files to space allocated
77
+ @file_list = Hash.new
78
+
79
+ # file basenames and date to check if files have already been
80
+ # allocated
81
+ @file_lup = Hash.new
82
+
83
+ if @file_list_path.nil? then
84
+ end
85
+
86
+
87
+
88
+
89
+ # ??
90
+ @alloc_lup = Hash.new
91
+ @total_space = 0
92
+ @total_alloc = 0
93
+
94
+ options[:unsafe] = true
95
+
96
+
97
+ # set up my own logger
98
+
99
+
100
+ @logger.debug "Added stores: #{@partitions.inspect}"
101
+
102
+ end
103
+
104
+ # return the free space (as a human readable string) in the system, or
105
+ # on the partition for the given path.
106
+ #
107
+ # @param [String] path to the
108
+ def free_space(path='')
109
+
110
+ get_and_lock_file_list do
111
+ update_space
112
+ fspace = 0
113
+ if path.empty? then
114
+ fspace = @total_space - @total_alloc
115
+ else
116
+ dev, space = free_space(path)
117
+ fspace = space - @alloc_lup[dev]
118
+ end
119
+ return Jeeves.human_size(fspace)
120
+ end
121
+
122
+ end
123
+
124
+
125
+ # allocate space for a video file with the given name, start time and duration
126
+ #
127
+ # use scale to adjust the default data rate if a tuner produces more or less.
128
+ # If the file is already allocated then do not allocate it again.
129
+ # Note that where a file already exists with the same name (for some reason?)
130
+ # a random key will be added, but the original name is remembered in case
131
+ # it is later used again.
132
+ #
133
+ # @param [String] name of the file, which should be a basename only
134
+ # @param [Integer] duration of the proposed recording in seconds
135
+ # @param [Time] for_when to allocate the space
136
+ def allocate(name, duration, for_when, scale=1.0)
137
+ space = 0
138
+ path = ''
139
+ dev = ''
140
+
141
+ get_and_lock_file_list do
142
+
143
+ # check if there is already a file for this programme
144
+ if @file_lup.has_key?(name) && @file_lup[name][:date] == for_when then
145
+ # we already have it so don't allocate again
146
+ @logger.info "File is already allocated: #{name} for #{for_when.strftime('%d-%b-%y %H:%M')}"
147
+ path = @file_lup[name][:path]
148
+ break
149
+ end
150
+
151
+ update_space # work out what space there is
152
+
153
+ # how much do we need
154
+ space = (duration * @base_rate * scale).to_i
155
+
156
+ # get the best drive for it
157
+ path = get_path(name, space)
158
+
159
+ #path = File.join(@partitions.first.path, name)
160
+
161
+ # does it already exist?
162
+ if file_exists_or_is_allocated?(path) then
163
+ # yes - so rename it
164
+ path = Jeeves.rename(path) {|f| file_exists_or_is_allocated?(f)}
165
+
166
+ end
167
+ # save it to the list
168
+ @file_list[path] = {:space=>space, :date=>for_when + duration}
169
+ @file_lup[name] = {:date=>for_when, :path=>path}
170
+ @total_alloc += space
171
+
172
+ @logger.info "Allocating #{space} kbytes for #{path} on #{dev}"
173
+
174
+ end
175
+
176
+ return path
177
+ end
178
+
179
+
180
+ # release allocated space, either because the file has been created
181
+ # or is no longer required
182
+ #
183
+ # @param [String] path to the file to be released
184
+ def release(path)
185
+ @logger.verbose "Attempting to release #{path}"
186
+ deleted = false
187
+ # open the list file to lock it for the duration
188
+ get_and_lock_file_list do
189
+
190
+ if @file_list.has_key?(path) then
191
+ # remove the space from the record of allocated space
192
+ @total_alloc -= @file_list[path][:space]
193
+ @file_list.delete(path)
194
+ @file_lup.delete(File.basename(path))
195
+ @logger.info "Deleted: #{path} from file_list"
196
+ deleted = true
197
+ else
198
+ @logger.verbose "Did not find #{path} to release"
199
+ end
200
+
201
+ end # auto closes the file
202
+ return deleted
203
+ end
204
+
205
+ def release_and_notify(path)
206
+
207
+ unless self.release(path)
208
+ # the file was never allocated? what
209
+ # to do now?
210
+ @logger.verbose "Trying to notify for a file that was not allocated: #{path}"
211
+ end
212
+
213
+ data = ''
214
+ @logger.info "Notifying Jeeves for #{path}"
215
+ open("#{@jeeves_url}/videos/new_from_file.txt?path=#{path}") do |resp|
216
+ resp.each_line do |line|
217
+ data << line
218
+ end
219
+ end
220
+ @logger.verbose "Jeeves returned #{data}"
221
+
222
+ return data == '0'
223
+
224
+ end
225
+
226
+ # return block for each allocated file with the path and a hash
227
+ # containing :space (in Kbytes) and :date when recording expected
228
+ def each_file(&block)
229
+ get_and_lock_file_list do
230
+ @file_list.each_pair do |key, value|
231
+ block.call(key, value)
232
+ end
233
+ end
234
+ end
235
+
236
+ # return true if there are no entries allocated
237
+ def empty?
238
+ empty = true
239
+ get_and_lock_file_list do
240
+ empty = @file_list.empty?
241
+ end
242
+ return empty
243
+ end
244
+
245
+ # return the number of entries in the file list
246
+ def files
247
+ len = 0
248
+ get_and_lock_file_list do
249
+ len = @file_list.length
250
+ end
251
+ return len
252
+ end
253
+
254
+ # return the number of stale files
255
+ #
256
+ # stale files are old and have no real file on the system
257
+ #
258
+ def stale_files
259
+ len = 0
260
+ get_and_lock_file_list do
261
+ @file_list.each_pair do |path, params|
262
+ len += 1 if params[:date] < Time.now
263
+ end
264
+ end
265
+ return len
266
+ end
267
+
268
+ # clear all allocated files etc. Should only do this if you are certain there
269
+ # are no outstanding recordings etc
270
+ def clean(all=false)
271
+ get_and_lock_file_list do
272
+ if all then
273
+ @file_list = Hash.new
274
+ else
275
+ @file_list.each_pair do |path, params|
276
+ if params[:date] < Time.now then
277
+ # already passed so lets clear it
278
+ @file_list.delete(path)
279
+ @file_list.delete(File.basename(path))
280
+ end
281
+ end
282
+ end # if all
283
+ end # lock flie list
284
+ end
285
+
286
+
287
+
288
+ # check that the specified partitions exist etc and add them to the store
289
+ #
290
+ # internal method called during initialization to set up the store.
291
+ # Checks that the partitions specified are properly mounted and ready
292
+ # (that the keys given match the partition), and then removes
293
+ # any duplicates that are on the same disk partition.
294
+ #
295
+ # It also looks for the file_list on each partition and if it finds it
296
+ # then it uses it and sets the partition as the master (has priority when
297
+ # removing duplicates). If there are two or more file_lists found then
298
+ # the most recent one is used. If there are none then one is created
299
+ # on the largest partition.
300
+ #
301
+ def check_partitions
302
+
303
+ @store_params.each_pair do |path, options|
304
+ begin
305
+ part = Jeeves::Partition.new(path, options)
306
+ if part.mounted? then
307
+ # path is OK, check it has been initialised with the given key
308
+ if part.ready? then
309
+ # it has, does it have a file_
310
+
311
+ @logger.info "Added #{path} to Jeeves Store"
312
+ @partitions << part
313
+
314
+ else
315
+ raise Jeeves::InvalidPartitionKey, "#{part.path} does not have a valid key"
316
+ end
317
+ else
318
+ raise Jeeves::InvalidPartition, "#{part.path} does not exist or is not mounted"
319
+ end
320
+ rescue Jeeves::InvalidPartition, Jeeves::InvalidPartitionKey => err
321
+ msg = "Jeeves Error with partition: #{err}"
322
+ case @store_errors
323
+ when :ignore
324
+ @logger.system msg
325
+ when :warn
326
+ @logger.warn msg
327
+ when :fatal
328
+ @logger.fatal msg + ", Aborting"
329
+ raise
330
+ end
331
+ end # begin block
332
+ end # do loop
333
+
334
+ if @partitions.length == 0 then
335
+ raise Jeeves::StoreError, "There are no partitions available"
336
+ else
337
+ @logger.verbose "There are #{@partitions.length} partitions specified"
338
+ end
339
+
340
+ # find the best file list!
341
+ master = nil
342
+ @partitions.each do |part|
343
+ if part.file_list? then
344
+ if master.nil? then
345
+ # no master yet so set this one to be it
346
+ master = part
347
+ else
348
+ # master exists but ignore it if its not the newest
349
+ master = part unless master.file_list_date > part.file_list_date
350
+ end
351
+ end
352
+ end
353
+
354
+ if master.nil?
355
+ @logger.verbose "No master partition found"
356
+ else
357
+ @logger.verbose "Master Partition set to #{master.path}"
358
+ end
359
+
360
+ # remove duplicates, giving precedence to the one with the most recent
361
+ # file list (master)
362
+ devices = Hash.new
363
+ @partitions.each do |part|
364
+ if devices.has_key?(part.device) then
365
+ # got one already
366
+ if part == master then
367
+ # but this one is better
368
+ old_part = devices[part.device]
369
+ devices[part.device] = part
370
+ @logger.verbose "Duplicate partition replaced: #{old_part.path}"
371
+ else
372
+ # duplicate, so ignore it
373
+ @logger.verbose "Duplicate partition: #{part.path}"
374
+ end
375
+ else
376
+ # not already there so save it...
377
+ devices[part.device] = part
378
+ @logger.verbose "Keeping unique partition #{part.path} on #{part.device}"
379
+ end
380
+ end # do
381
+
382
+ @partitions = devices.values
383
+
384
+ update_space
385
+
386
+ if master.nil? then
387
+ # did not find a partition with a file list so create one
388
+ # the first partition should be the largest
389
+ # not found a filelist, so create one
390
+ @logger.debug "No file list found"
391
+ @file_list_path = @partitions.first.file_list
392
+ save_file_list
393
+ @logger.debug "Created file list at #{@file_list_path}"
394
+ else
395
+ @file_list_path = master.file_list
396
+ end
397
+
398
+ @logger.info "Using file list: #{@file_list_path}"
399
+
400
+ end
401
+
402
+ def each_partition
403
+ @partitions.each do |part|
404
+ yield part
405
+ end
406
+ end
407
+
408
+ private
409
+ # wrapper around the file locking business
410
+ def get_and_lock_file_list(&block)
411
+ File.open(@file_list_path, 'r+') do |flist|
412
+ flist.flock(File::LOCK_EX)
413
+ begin
414
+ saved_data = YAML.load(flist)
415
+ unless saved_data[:version_major] == Jeeves::Version.split('.').first
416
+ raise VersionError, "Saved data is incompatible with current version"
417
+ end
418
+ load_saved_data(saved_data)
419
+
420
+ # do whatever
421
+ yield
422
+
423
+ # empty the file before re-writing it
424
+ flist.truncate(0)
425
+ flist.rewind
426
+ YAML::dump(save_data, flist)
427
+ ensure
428
+ flist.flock(File::LOCK_UN)
429
+ end
430
+ end # auto closes the file
431
+
432
+ end
433
+
434
+ # update the free space records
435
+ def update_space
436
+ @total_space = 0
437
+ # get actual free space from df
438
+ @partitions.each do |part|
439
+ part.update_space # in case it has been a while?
440
+ #dev, space = free_space(store)
441
+ @logger.debug "Updating space for store #{part.path}: #{part.free_space}"
442
+ @dev_lup[part.path] = part.device
443
+ @space_lup[part.device] = part.free_space
444
+ @total_space += part.free_space
445
+ end
446
+ # now deduct stuff that has been allocated
447
+ if @file_list then
448
+ @file_list.each_pair do |key, value|
449
+ @logger.debug "Deducting allocated file: #{key}, #{value}"
450
+ # dev = @dev_lup[File.dirname(key)]
451
+ # @space_lup[dev] -= value
452
+ end
453
+ end
454
+ # now sort the partitions by free space
455
+ @partitions.sort! {|x,y| x.free_space <=> y.free_space}
456
+
457
+ end
458
+
459
+ # return the free space in 1K blocks on the given path
460
+ # def free_space(path)
461
+ # # use df to get the free space for path
462
+ # df_lines = `/bin/df -Pk #{path}`.split("\n")
463
+ # @logger.debug "df yeilds: " + df_lines.inspect
464
+ # df_fields = df_lines[1].split
465
+ # return [df_fields[0], df_fields[3].to_i]
466
+ # end
467
+ #
468
+
469
+ # @file_list contains a list of file data for requests made but
470
+ # where files have not yet been created. Need to save
471
+ # this information to disk to retain over restarts
472
+ #
473
+ # load a saved file list if there was one
474
+ def load_file_list
475
+ if FileTest.exists?(@file_list_path) then
476
+ @file_list = File.open(@file_list_path) {|yf| YAML.load(yf)}
477
+ end
478
+
479
+ # update allocated space
480
+ @total_alloc = 0
481
+ @file_list.each do |file, space|
482
+ #dev, dev_space = free_space(File.dirname(file))
483
+ @total_alloc += space
484
+ end
485
+
486
+ end
487
+
488
+ # load the saved data into instance variables
489
+ def load_saved_data(saved_data)
490
+ @file_list = saved_data[:file_list] || Hash.new
491
+ @file_lup = saved_data[:file_lup] || Hash.new
492
+ end
493
+
494
+ def save_data
495
+ saved_data = Hash.new
496
+ saved_data[:version_major] = Jeeves::Version.split('.').first
497
+ saved_data[:file_list] = @file_list || Hash.new
498
+ saved_data[:file_lup] = @file_lup || Hash.new
499
+ @logger.debug "Saving #{saved_data.inspect}"
500
+ return saved_data
501
+ end
502
+
503
+ # save the file list
504
+ def save_file_list
505
+ File.open(@file_list_path, 'w') do |yf|
506
+ yf.flock(File::LOCK_EX)
507
+ begin
508
+ YAML::dump(save_data, yf)
509
+ ensure
510
+ yf.flock(File::LOCK_UN)
511
+ end
512
+ end
513
+ end
514
+
515
+ # find the best place to put the file
516
+ def get_path(name, space)
517
+ best_dev = @space_lup.sort {|a,b| b[1] <=> a[1]}.first[0]
518
+ @logger.debug "#{best_dev} has the most space left"
519
+ path = @dev_lup.key(best_dev)
520
+ return File.join(path, name)
521
+ end
522
+
523
+ def file_exists_or_is_allocated?(path)
524
+ File.exists?(path) || @file_list.has_key?(path)
525
+ end
526
+
527
+ def rename(path)
528
+ dir_name = File.dirname(path)
529
+ extname = File.extname(path)
530
+ basename = File.basename(path, extname)
531
+
532
+ while file_exists?(path)
533
+ # oh dear, got one already. need to add something to the name
534
+ randname = (rand * 10000).to_i.to_s
535
+ basename = basename + '_' + randname + extname
536
+ path = File.join(dir_name, basename)
537
+ end
538
+
539
+ return path
540
+
541
+ end
542
+
543
+ end
544
+ end