directory_watcher 1.4.1 → 1.5.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/.gitignore +17 -0
- data/History.txt +10 -0
- data/README.txt +1 -1
- data/Rakefile +16 -5
- data/lib/directory_watcher.rb +166 -139
- data/lib/directory_watcher/collector.rb +283 -0
- data/lib/directory_watcher/configuration.rb +228 -0
- data/lib/directory_watcher/coolio_scanner.rb +61 -127
- data/lib/directory_watcher/em_scanner.rb +81 -153
- data/lib/directory_watcher/event.rb +72 -0
- data/lib/directory_watcher/eventable_scanner.rb +242 -0
- data/lib/directory_watcher/file_stat.rb +65 -0
- data/lib/directory_watcher/logable.rb +26 -0
- data/lib/directory_watcher/notifier.rb +49 -0
- data/lib/directory_watcher/paths.rb +55 -0
- data/lib/directory_watcher/rev_scanner.rb +68 -131
- data/lib/directory_watcher/scan.rb +72 -0
- data/lib/directory_watcher/scan_and_queue.rb +22 -0
- data/lib/directory_watcher/scanner.rb +26 -209
- data/lib/directory_watcher/threaded.rb +277 -0
- data/lib/directory_watcher/version.rb +8 -0
- data/spec/directory_watcher_spec.rb +37 -0
- data/spec/paths_spec.rb +7 -0
- data/spec/scanner_scenarios.rb +236 -0
- data/spec/spec_helper.rb +79 -0
- data/spec/utility_classes.rb +117 -0
- data/version.txt +1 -1
- metadata +123 -23
- data/bin/dw +0 -2
@@ -0,0 +1,283 @@
|
|
1
|
+
# Collector reads items from a collection Queue and processes them to see if
|
2
|
+
# FileEvents should be put onto the notification Queue.
|
3
|
+
#
|
4
|
+
class DirectoryWatcher::Collector
|
5
|
+
include DirectoryWatcher::Threaded
|
6
|
+
include DirectoryWatcher::Logable
|
7
|
+
|
8
|
+
# Create a new StatCollector from the given Configuration, and an optional
|
9
|
+
# Scan.
|
10
|
+
#
|
11
|
+
# configuration - The Collector uses from Configuration:
|
12
|
+
# collection_queue - The Queue to read items from the Scanner on
|
13
|
+
# notification_queue - The Queue to submit the Events to the Notifier on
|
14
|
+
# stable - The number of times we see a file hasn't changed before
|
15
|
+
# emitting a stable event
|
16
|
+
# sort_by - the method used to sort events during on_scan results
|
17
|
+
# order_by - The method used to order events from call to on_scan
|
18
|
+
#
|
19
|
+
# pre_load_scan - A Scan to use to load our internal state from before. No
|
20
|
+
# events will be emitted for the FileStat's in this scan.
|
21
|
+
#
|
22
|
+
#def initialize( notification_queue, collection_queue, options = {} )
|
23
|
+
def initialize( config )
|
24
|
+
@stats = Hash.new
|
25
|
+
@stable_counts = Hash.new
|
26
|
+
@config = config
|
27
|
+
on_scan( DirectoryWatcher::Scan.new( config.glob ), false ) if config.pre_load?
|
28
|
+
self.interval = 0.01 # yes this is a fast loop
|
29
|
+
end
|
30
|
+
|
31
|
+
# The number of times we see a file hasn't changed before emitting a stable
|
32
|
+
# count. See Configuration#stable
|
33
|
+
def stable_threshold
|
34
|
+
@config.stable
|
35
|
+
end
|
36
|
+
|
37
|
+
# How to sort Scan results. See Configuration.
|
38
|
+
#
|
39
|
+
def sort_by
|
40
|
+
@config.sort_by
|
41
|
+
end
|
42
|
+
|
43
|
+
# How to order Scan results. See Configuration.
|
44
|
+
#
|
45
|
+
def order_by
|
46
|
+
@config.order_by
|
47
|
+
end
|
48
|
+
|
49
|
+
# The queue from which to read items from the scanners. See Configuration.
|
50
|
+
#
|
51
|
+
def collection_queue
|
52
|
+
@config.collection_queue
|
53
|
+
end
|
54
|
+
|
55
|
+
# The queue to write Events for the Notifier. See Configuration.
|
56
|
+
#
|
57
|
+
def notification_queue
|
58
|
+
@config.notification_queue
|
59
|
+
end
|
60
|
+
|
61
|
+
# Given the scan, update the set of stats with the results from the Scan and
|
62
|
+
# emit events to the notification queue as appropriate.
|
63
|
+
#
|
64
|
+
# scan - The Scan containing all the new FileStat items
|
65
|
+
# emit_events - Should events be emitted for the events in the scan
|
66
|
+
# (default: true)
|
67
|
+
#
|
68
|
+
# There is one odd thing that happens here. Scanners that are EventableScanners
|
69
|
+
# use on_stat to emit removed events, and the standard threaded Scanner only
|
70
|
+
# uses Scans. So we make sure and only emit removed events in this method if
|
71
|
+
# the scanner that gave us the scan was the basic threaded Scanner.
|
72
|
+
#
|
73
|
+
# TODO: Possibly fix this through another abstraction in the Scanners.
|
74
|
+
# No idea about what that would be yet.
|
75
|
+
#
|
76
|
+
# Returns nothing.
|
77
|
+
#
|
78
|
+
def on_scan( scan, emit_events = true )
|
79
|
+
seen_paths = Set.new
|
80
|
+
logger.debug "Sorting by #{sort_by} #{order_by}"
|
81
|
+
sorted_stats( scan.run ).each do |stat|
|
82
|
+
on_stat(stat, emit_events)
|
83
|
+
seen_paths << stat.path
|
84
|
+
end
|
85
|
+
emit_removed_events(seen_paths) if @config.scanner.nil?
|
86
|
+
end
|
87
|
+
|
88
|
+
# Process a single stat and emit an event if necessary.
|
89
|
+
#
|
90
|
+
# stat - The new FileStat to process and see if an event should
|
91
|
+
# be emitted
|
92
|
+
# emit_event - Whether or not an event should be emitted.
|
93
|
+
#
|
94
|
+
# Returns nothing
|
95
|
+
def on_stat( stat, emit_event = true )
|
96
|
+
orig_stat = update_stat( stat )
|
97
|
+
logger.debug "Emitting event for on_stat #{stat}"
|
98
|
+
emit_event_for( orig_stat, stat ) if emit_event
|
99
|
+
end
|
100
|
+
|
101
|
+
# Remove one item from the collection queue and process it.
|
102
|
+
#
|
103
|
+
# This method is required by the Threaded API
|
104
|
+
#
|
105
|
+
# Returns nothing
|
106
|
+
def run
|
107
|
+
case thing = collection_queue.deq
|
108
|
+
when ::DirectoryWatcher::Scan
|
109
|
+
on_scan(thing)
|
110
|
+
when ::DirectoryWatcher::FileStat
|
111
|
+
on_stat(thing)
|
112
|
+
else
|
113
|
+
raise "Unknown item in the queue: #{thing}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Write the current stats to the given IO object as a YAML document.
|
118
|
+
#
|
119
|
+
# io - The IO object to write the document to.
|
120
|
+
#
|
121
|
+
# Returns nothing.
|
122
|
+
def dump_stats( io )
|
123
|
+
YAML.dump(@stats, io)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Read the current stats from the given IO object. Any existing stats in the
|
127
|
+
# Collector will be overwritten
|
128
|
+
#
|
129
|
+
# io - The IO object from which to read the document.
|
130
|
+
#
|
131
|
+
# Returns nothing.
|
132
|
+
def load_stats( io )
|
133
|
+
@stats = YAML.load(io)
|
134
|
+
end
|
135
|
+
|
136
|
+
#######
|
137
|
+
private
|
138
|
+
#######
|
139
|
+
|
140
|
+
# Sort the stats by +sort_by+ and +order_by+ returning the results
|
141
|
+
#
|
142
|
+
def sorted_stats( stats )
|
143
|
+
sorted = stats.sort_by{ |stat| stat.send(sort_by) }
|
144
|
+
sorted = sorted.reverse if order_by == :descending
|
145
|
+
return sorted
|
146
|
+
end
|
147
|
+
|
148
|
+
# Update the stats Hash with the new_stat information, return the old data
|
149
|
+
# that is being replaced.
|
150
|
+
#
|
151
|
+
def update_stat( new_stat )
|
152
|
+
old_stat = @stats.delete(new_stat.path)
|
153
|
+
@stats.store(new_stat.path, new_stat) unless new_stat.removed?
|
154
|
+
return old_stat
|
155
|
+
end
|
156
|
+
|
157
|
+
# Look for removed files and emit removed events for all of them.
|
158
|
+
#
|
159
|
+
# seen_paths - the list of files that we know currently exist
|
160
|
+
#
|
161
|
+
# Return nothing
|
162
|
+
def emit_removed_events( seen_paths )
|
163
|
+
@stats.keys.each do |existing_path|
|
164
|
+
next if seen_paths.include?(existing_path)
|
165
|
+
old_stat = @stats.delete(existing_path)
|
166
|
+
emit_event_for(old_stat, ::DirectoryWatcher::FileStat.for_removed_path(existing_path))
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Determine what type of event to emit, and put that event onto the
|
171
|
+
# notification queue.
|
172
|
+
#
|
173
|
+
# old_stat - The old FileStat
|
174
|
+
# new_stat - The new FileStat
|
175
|
+
#
|
176
|
+
# Returns nothing
|
177
|
+
def emit_event_for( old_stat, new_stat )
|
178
|
+
event = DirectoryWatcher::Event.from_stats( old_stat, new_stat )
|
179
|
+
if should_emit?(event) then
|
180
|
+
logger.debug "Sending event #{event.object_id} to notifcation queue"
|
181
|
+
notification_queue.enq( event )
|
182
|
+
else
|
183
|
+
logger.debug "Emitting of event #{event.object_id} cancelled"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Should the event given actually be emitted.
|
188
|
+
#
|
189
|
+
# If the event passed in is NOT a stable event, return true
|
190
|
+
# If there is a stable_threshold, then check to see if the stable count for
|
191
|
+
# this event's path has crossed the stable threshold.
|
192
|
+
#
|
193
|
+
# This method has the side effect of updating the stable count of the path of
|
194
|
+
# the event. If we are going to return true for the stable event, then we
|
195
|
+
# reset the stable count of that event to 0.
|
196
|
+
#
|
197
|
+
# event - any event
|
198
|
+
#
|
199
|
+
# Returns whether or not to emit the event based upon its stability
|
200
|
+
def should_emit?( event )
|
201
|
+
if event.stable? then
|
202
|
+
if emitting_stable_events? and valid_for_stable_event?( event.path )then
|
203
|
+
increment_stable_count( event.path )
|
204
|
+
if should_emit_stable?( event.path ) then
|
205
|
+
mark_as_invalid_for_stable_event( event.path )
|
206
|
+
return true
|
207
|
+
end
|
208
|
+
end
|
209
|
+
return false
|
210
|
+
elsif event.removed? then
|
211
|
+
mark_as_invalid_for_stable_event( event.path )
|
212
|
+
return true
|
213
|
+
else
|
214
|
+
mark_as_valid_for_stable_event( event.path )
|
215
|
+
return true
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Is the given path able to have a stable event emitted for it?
|
220
|
+
#
|
221
|
+
# A stable event may only be emitted for a path that has already had an added
|
222
|
+
# or modified event already sent. Also, once a stable event has been emitted
|
223
|
+
# for a path, another stable event may not be emitted until it has been
|
224
|
+
# modified, or added again.
|
225
|
+
#
|
226
|
+
# path - the path of the file to check
|
227
|
+
#
|
228
|
+
# Returns whether or not the path may have a stable event emitted for it.
|
229
|
+
def valid_for_stable_event?( path )
|
230
|
+
@stable_counts.has_key?( path )
|
231
|
+
end
|
232
|
+
|
233
|
+
# Let it be known that the given path can now have a stable event emitted for
|
234
|
+
# it.
|
235
|
+
#
|
236
|
+
# path - the path to mark as ready
|
237
|
+
#
|
238
|
+
# Returns nothing
|
239
|
+
def mark_as_valid_for_stable_event( path )
|
240
|
+
logger.debug "#{path} marked as valid for stable"
|
241
|
+
@stable_counts[path] = 0
|
242
|
+
end
|
243
|
+
|
244
|
+
# Mark that the given path is invalid for having a stable event emitted for
|
245
|
+
# it.
|
246
|
+
#
|
247
|
+
# path - the path to mark
|
248
|
+
#
|
249
|
+
# Returns nothing
|
250
|
+
def mark_as_invalid_for_stable_event( path )
|
251
|
+
logger.debug "#{path} marked as invalid for stable"
|
252
|
+
@stable_counts.delete( path )
|
253
|
+
end
|
254
|
+
|
255
|
+
# Increment the stable count for the given path
|
256
|
+
#
|
257
|
+
# path - the path of the file to increment its stable count
|
258
|
+
#
|
259
|
+
# Returns nothing
|
260
|
+
def increment_stable_count( path )
|
261
|
+
@stable_counts[path] += 1
|
262
|
+
end
|
263
|
+
|
264
|
+
# Is the given path ready to have a stable event emitted?
|
265
|
+
#
|
266
|
+
# path - the path to report on
|
267
|
+
#
|
268
|
+
# Returns whether to emit a stable event or not
|
269
|
+
def should_emit_stable?( path )
|
270
|
+
@stable_counts[path] >= stable_threshold
|
271
|
+
end
|
272
|
+
|
273
|
+
# Is it legal for us to emit stable events at all. This checks the config to
|
274
|
+
# see if that is the case.
|
275
|
+
#
|
276
|
+
# In the @config if the stable threshold is set then we are emitting stable
|
277
|
+
# events.
|
278
|
+
#
|
279
|
+
# Returns whether it is legal to propogate stable events
|
280
|
+
def emitting_stable_events?
|
281
|
+
stable_threshold
|
282
|
+
end
|
283
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
#
|
2
|
+
# The top level configuration options used by DirectoryWatcher are used by many
|
3
|
+
# of the sub components for a variety of purposes. The Configuration represents
|
4
|
+
# all those options and other global like instances.
|
5
|
+
#
|
6
|
+
# The top level DirectoryWatcher class allows the configs to be changed during
|
7
|
+
# execution, so all of the dependent classes need to be informed when their
|
8
|
+
# options have changed. This class allows that.
|
9
|
+
#
|
10
|
+
class DirectoryWatcher::Configuration
|
11
|
+
# The directory to monitor for events. The glob's will be used in conjunction
|
12
|
+
# with this directory to find the full list of globs available.
|
13
|
+
attr_reader :dir
|
14
|
+
|
15
|
+
# The glob of files to monitor. This is an Array of file matching globs
|
16
|
+
# be aware that changing the :glob value after watching has started has the
|
17
|
+
# potential to cause spurious events if the new globs do not match the old,
|
18
|
+
# files will appear to have been deleted.
|
19
|
+
#
|
20
|
+
# The default is '*'
|
21
|
+
attr_reader :glob
|
22
|
+
|
23
|
+
# The interval at which to do a full scan using the +glob+ to determine Events
|
24
|
+
# to send.
|
25
|
+
#
|
26
|
+
# The default is 30.0 seconds
|
27
|
+
attr_reader :interval
|
28
|
+
|
29
|
+
# Controls the number of intervals a file must remain unchanged before it is
|
30
|
+
# considered "stable". When this condition is met, a stable event is
|
31
|
+
# generated for the file. If stable is set to +nil+ then stable events
|
32
|
+
# will not be generated.
|
33
|
+
#
|
34
|
+
# The default is nil, indicating no stable events are to be emitted.
|
35
|
+
attr_reader :stable
|
36
|
+
|
37
|
+
# pre_load says if an initial scan using the globs should be done to pre
|
38
|
+
# populate the state of the system before sending any events.
|
39
|
+
#
|
40
|
+
# The default is false
|
41
|
+
attr_reader :pre_load
|
42
|
+
|
43
|
+
# The filename to persist the state of the DirectoryWatcher too upon calling
|
44
|
+
# *stop*.
|
45
|
+
#
|
46
|
+
# The default is nil, indicating that no state is to be persisted.
|
47
|
+
attr_reader :persist
|
48
|
+
|
49
|
+
# The back end scanner to use. The available options are:
|
50
|
+
#
|
51
|
+
# nil => Use the default, pure ruby Threaded scanner
|
52
|
+
# :em => Use the EventMachine based scanner. This requires that the
|
53
|
+
# 'eventmachine' gem be installed.
|
54
|
+
# :coolio => Use the Cool.io based scanner. This requires that the
|
55
|
+
# 'cool.io' gem be installed.
|
56
|
+
# :rev => Use the Rev based scanner. This requires that the 'rev' gem be
|
57
|
+
# installed.
|
58
|
+
#
|
59
|
+
# The default is nil, indicating the pure ruby threaded scanner will be used.
|
60
|
+
# This option may not be changed once the DirectoryWatcher is allocated.
|
61
|
+
#
|
62
|
+
attr_reader :scanner
|
63
|
+
|
64
|
+
# The sorting method to use when emitting a set of Events after a Scan has
|
65
|
+
# happened. Since a Scan may produce a number of events, if those Events should
|
66
|
+
# be emitted in a particular order, use +sort_by+ to pick which field to sort
|
67
|
+
# the events, and +order_by+ to say if those events are to be emitted in
|
68
|
+
# :ascending or :descending order.
|
69
|
+
#
|
70
|
+
# Available options:
|
71
|
+
#
|
72
|
+
# :path => The default, they will be sorted by full pathname
|
73
|
+
# :mtime => Last modified time. They will be sorted by their FileStat mtime
|
74
|
+
# :size => The number of bytes in the file.
|
75
|
+
#
|
76
|
+
attr_accessor :sort_by
|
77
|
+
|
78
|
+
# When sorting you may pick if the order should be:
|
79
|
+
#
|
80
|
+
# :ascending => The default, from lowest to highest
|
81
|
+
# :descending => from highest to lowest.
|
82
|
+
#
|
83
|
+
attr_accessor :order_by
|
84
|
+
|
85
|
+
# The Queue through which the Scanner will send data to the Collector
|
86
|
+
#
|
87
|
+
attr_reader :collection_queue
|
88
|
+
|
89
|
+
# The Queue through which the Collector will send data to the Notifier
|
90
|
+
#
|
91
|
+
attr_reader :notification_queue
|
92
|
+
|
93
|
+
# The logger through wich every one will log
|
94
|
+
#
|
95
|
+
attr_reader :logger
|
96
|
+
|
97
|
+
# Return a Hash of all the default options
|
98
|
+
#
|
99
|
+
def self.default_options
|
100
|
+
{
|
101
|
+
:dir => '.',
|
102
|
+
:glob => '*',
|
103
|
+
:interval => 30.0,
|
104
|
+
:stable => nil,
|
105
|
+
:pre_load => false,
|
106
|
+
:persist => nil,
|
107
|
+
:scanner => nil,
|
108
|
+
:sort_by => :path,
|
109
|
+
:order_by => :ascending,
|
110
|
+
:logger => nil,
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
# Create a new Configuration by blending the passed in items with the defaults
|
115
|
+
#
|
116
|
+
def initialize( options = {} )
|
117
|
+
o = self.class.default_options.merge( options )
|
118
|
+
@dir = o[:dir]
|
119
|
+
@pre_load = o[:pre_load]
|
120
|
+
@scanner = o[:scanner]
|
121
|
+
@sort_by = o[:sort_by]
|
122
|
+
@order_by = o[:order_by]
|
123
|
+
|
124
|
+
# These have validation rules
|
125
|
+
self.persist = o[:persist]
|
126
|
+
self.interval = o[:interval]
|
127
|
+
self.glob = o[:glob]
|
128
|
+
self.stable = o[:stable]
|
129
|
+
self.logger = o[:logger]
|
130
|
+
|
131
|
+
@notification_queue = Queue.new
|
132
|
+
@collection_queue = Queue.new
|
133
|
+
end
|
134
|
+
|
135
|
+
# Is pre_load set or not
|
136
|
+
#
|
137
|
+
def pre_load?
|
138
|
+
@pre_load
|
139
|
+
end
|
140
|
+
|
141
|
+
# The class of the scanner
|
142
|
+
#
|
143
|
+
def scanner_class
|
144
|
+
class_name = scanner.to_s.capitalize + 'Scanner'
|
145
|
+
DirectoryWatcher.const_get( class_name ) rescue DirectoryWatcher::Scanner
|
146
|
+
end
|
147
|
+
|
148
|
+
# call-seq:
|
149
|
+
# glob = '*'
|
150
|
+
# glob = ['lib/**/*.rb', 'test/**/*.rb']
|
151
|
+
#
|
152
|
+
# Sets the glob pattern that will be used when scanning the directory for
|
153
|
+
# files. A single glob pattern can be given or an array of glob patterns.
|
154
|
+
#
|
155
|
+
def glob=( val )
|
156
|
+
glob = case val
|
157
|
+
when String; [File.join(@dir, val)]
|
158
|
+
when Array; val.flatten.map! {|g| File.join(@dir, g)}
|
159
|
+
else
|
160
|
+
raise(ArgumentError,
|
161
|
+
'expecting a glob pattern or an array of glob patterns')
|
162
|
+
end
|
163
|
+
glob.uniq!
|
164
|
+
@glob = glob
|
165
|
+
end
|
166
|
+
|
167
|
+
# Sets the directory scan interval. The directory will be scanned every
|
168
|
+
# _interval_ seconds for changes to files matching the glob pattern.
|
169
|
+
# Raises +ArgumentError+ if the interval is zero or negative.
|
170
|
+
#
|
171
|
+
def interval=( val )
|
172
|
+
val = Float(val)
|
173
|
+
raise ArgumentError, "interval must be greater than zero" if val <= 0
|
174
|
+
@interval = val
|
175
|
+
end
|
176
|
+
|
177
|
+
# Sets the logger instance. This will be used by all classes for logging
|
178
|
+
#
|
179
|
+
def logger=( val )
|
180
|
+
if val then
|
181
|
+
if %w[ debug info warn error fatal ].all? { |meth| val.respond_to?( meth ) } then
|
182
|
+
@logger = val
|
183
|
+
end
|
184
|
+
else
|
185
|
+
@logger = ::DirectoryWatcher::Logable.default_logger
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Sets the number of intervals a file must remain unchanged before it is
|
190
|
+
# considered "stable". When this condition is met, a stable event is
|
191
|
+
# generated for the file. If stable is set to +nil+ then stable events
|
192
|
+
# will not be generated.
|
193
|
+
#
|
194
|
+
# A stable event will be generated once for a file. Another stable event
|
195
|
+
# will only be generated after the file has been modified and then remains
|
196
|
+
# unchanged for _stable_ intervals.
|
197
|
+
#
|
198
|
+
# Example:
|
199
|
+
#
|
200
|
+
# dw = DirectoryWatcher.new( '/tmp', :glob => 'swap.*' )
|
201
|
+
# dw.interval = 15.0
|
202
|
+
# dw.stable = 4
|
203
|
+
#
|
204
|
+
# In this example, a directory watcher is configured to look for swap files
|
205
|
+
# in the /tmp directory. Stable events will be generated every 4 scan
|
206
|
+
# intervals iff a swap remains unchanged for that time. In this case the
|
207
|
+
# time is 60 seconds (15.0 * 4).
|
208
|
+
#
|
209
|
+
def stable=( val )
|
210
|
+
if val.nil?
|
211
|
+
@stable = nil
|
212
|
+
else
|
213
|
+
val = Integer(val)
|
214
|
+
raise ArgumentError, "stable must be greater than zero" if val <= 0
|
215
|
+
@stable = val
|
216
|
+
end
|
217
|
+
return @stable
|
218
|
+
end
|
219
|
+
|
220
|
+
# Sets the name of the file to which the directory watcher state will be
|
221
|
+
# persisted when it is stopped. Setting the persist filename to +nil+ will
|
222
|
+
# disable this feature.
|
223
|
+
#
|
224
|
+
def persist=( filename )
|
225
|
+
@persist = filename ? filename.to_s : nil
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|