jeeves-pvr 0.2.0

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,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