rbbt-util 5.28.10 → 5.29.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/rbbt/hpc.rb +1 -549
- data/lib/rbbt/hpc/orchestrate.rb +24 -0
- data/lib/rbbt/hpc/slurm.rb +570 -0
- data/lib/rbbt/persist.rb +8 -3
- data/lib/rbbt/resource.rb +12 -6
- data/lib/rbbt/resource/path.rb +1 -1
- data/lib/rbbt/tsv/attach.rb +7 -4
- data/lib/rbbt/tsv/parallel.rb +0 -3
- data/lib/rbbt/util/R.rb +2 -2
- data/lib/rbbt/util/cmd.rb +9 -0
- data/lib/rbbt/util/misc/indiferent_hash.rb +8 -0
- data/lib/rbbt/util/misc/inspect.rb +23 -9
- data/lib/rbbt/workflow.rb +2 -1
- data/lib/rbbt/workflow/accessor.rb +8 -2
- data/lib/rbbt/workflow/definition.rb +1 -0
- data/lib/rbbt/workflow/examples.rb +2 -2
- data/lib/rbbt/workflow/step.rb +12 -6
- data/lib/rbbt/workflow/step/accessor.rb +47 -27
- data/lib/rbbt/workflow/step/dependencies.rb +9 -4
- data/lib/rbbt/workflow/step/run.rb +22 -20
- data/lib/rbbt/workflow/util/orchestrator.rb +14 -9
- data/lib/rbbt/workflow/util/provenance.rb +12 -5
- data/share/rbbt_commands/slurm/list +141 -0
- data/share/rbbt_commands/slurm/orchestrate +47 -0
- data/share/rbbt_commands/{workflow/slurm → slurm/task} +10 -3
- data/share/rbbt_commands/system/status +22 -22
- data/share/rbbt_commands/workflow/info +12 -9
- data/share/rbbt_commands/workflow/prov +2 -1
- data/test/rbbt/test_workflow.rb +36 -4
- data/test/rbbt/tsv/test_attach.rb +86 -6
- metadata +7 -3
data/lib/rbbt/persist.rb
CHANGED
@@ -26,12 +26,17 @@ module Persist
|
|
26
26
|
MAX_FILE_LENGTH = 150
|
27
27
|
|
28
28
|
# Is 'file' newer than 'path'? return non-true if path is newer than file
|
29
|
-
def self.newer?(path, file)
|
29
|
+
def self.newer?(path, file, by_link = false)
|
30
30
|
return true if not Open.exists?(file)
|
31
31
|
path = path.find if Path === path
|
32
32
|
file = file.find if Path === file
|
33
|
-
|
34
|
-
|
33
|
+
if by_link
|
34
|
+
patht = File.exists?(path) ? File.lstat(path).mtime : nil
|
35
|
+
filet = File.exists?(file) ? File.lstat(file).mtime : nil
|
36
|
+
else
|
37
|
+
patht = Open.mtime(path)
|
38
|
+
filet = Open.mtime(file)
|
39
|
+
end
|
35
40
|
return true if patht.nil? || filet.nil?
|
36
41
|
diff = patht - filet
|
37
42
|
return diff if diff < 0
|
data/lib/rbbt/resource.rb
CHANGED
@@ -112,13 +112,19 @@ module Resource
|
|
112
112
|
end
|
113
113
|
when Net::HTTPRedirection, Net::HTTPFound
|
114
114
|
location = response['location']
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
115
|
+
if location.include? 'get_directory'
|
116
|
+
Log.debug("Feching directory from: #{location}. Into: #{final_path}")
|
117
|
+
FileUtils.mkdir_p final_path unless File.exist? final_path
|
118
|
+
TmpFile.with_file do |tmp_dir|
|
119
|
+
Misc.in_dir tmp_dir do
|
120
|
+
CMD.cmd('tar xvfz -', :in => Open.open(location, :nocache => true))
|
121
|
+
end
|
122
|
+
FileUtils.mv tmp_dir, final_path
|
123
|
+
end
|
124
|
+
else
|
125
|
+
Open.open(location, :nocache => true) do |s|
|
126
|
+
Misc.sensiblewrite(final_path, s)
|
120
127
|
end
|
121
|
-
FileUtils.mv tmp_dir, final_path
|
122
128
|
end
|
123
129
|
when Net::HTTPInternalServerError
|
124
130
|
@server_missing_resource_cache << url
|
data/lib/rbbt/resource/path.rb
CHANGED
data/lib/rbbt/tsv/attach.rb
CHANGED
@@ -243,6 +243,7 @@ module TSV
|
|
243
243
|
Log.debug("Attachment of fields:#{Misc.fingerprint fields } from #{other.filename.inspect} finished.")
|
244
244
|
|
245
245
|
if complete
|
246
|
+
Log.warn "Attaching through index and completing empty rows; keys with wrong format may appear (#{other.key_field} insted of #{self.key_field})" if index
|
246
247
|
fill = TrueClass === complete ? nil : complete
|
247
248
|
field_length = self.fields.length
|
248
249
|
common_fields = (other.fields & self.fields)
|
@@ -255,11 +256,11 @@ module TSV
|
|
255
256
|
case type
|
256
257
|
when :single
|
257
258
|
missing.each do |k|
|
258
|
-
self[k] =
|
259
|
+
self[k] = fill
|
259
260
|
end
|
260
261
|
when :list
|
261
262
|
missing.each do |k|
|
262
|
-
values = [
|
263
|
+
values = [fill] * field_length
|
263
264
|
other_values = other[k]
|
264
265
|
other_common_pos.zip(this_common_pos).each do |o,t|
|
265
266
|
values[t] = other_values[o]
|
@@ -267,8 +268,9 @@ module TSV
|
|
267
268
|
self[k] = values
|
268
269
|
end
|
269
270
|
when :double
|
271
|
+
fill = [] if fill.nil?
|
270
272
|
missing.each do |k|
|
271
|
-
values = [
|
273
|
+
values = [fill] * field_length
|
272
274
|
other_values = other[k]
|
273
275
|
other_common_pos.zip(this_common_pos).each do |o,t|
|
274
276
|
values[t] = other_values[o]
|
@@ -276,8 +278,9 @@ module TSV
|
|
276
278
|
self[k] = values
|
277
279
|
end
|
278
280
|
when :flat
|
281
|
+
fill = [] if fill.nil?
|
279
282
|
missing.each do |k|
|
280
|
-
self[k] =
|
283
|
+
self[k] = fill
|
281
284
|
end
|
282
285
|
end
|
283
286
|
end
|
data/lib/rbbt/tsv/parallel.rb
CHANGED
data/lib/rbbt/util/R.rb
CHANGED
@@ -41,7 +41,7 @@ source('#{UTIL}');
|
|
41
41
|
|
42
42
|
if monitor
|
43
43
|
#io = CMD.cmd('R --no-save --quiet', options.merge(:in => cmd, :pipe => true, :log => true))
|
44
|
-
io = CMD.cmd('R --no-save --quiet', options.merge(:in => cmd, :pipe => true, :log => true))
|
44
|
+
io = CMD.cmd('R --no-save --quiet', options.merge(:in => cmd, :pipe => true, :log => true, :xvfb => true))
|
45
45
|
while line = io.gets
|
46
46
|
case monitor
|
47
47
|
when Proc
|
@@ -52,7 +52,7 @@ source('#{UTIL}');
|
|
52
52
|
end
|
53
53
|
nil
|
54
54
|
else
|
55
|
-
CMD.cmd('R --no-save --slave --quiet', options.merge(:in => cmd))
|
55
|
+
CMD.cmd('R --no-save --slave --quiet', options.merge(:in => cmd, :xvfb => true))
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
data/lib/rbbt/util/cmd.rb
CHANGED
@@ -100,6 +100,7 @@ module CMD
|
|
100
100
|
no_fail = options.delete(:no_fail)
|
101
101
|
no_fail = options.delete(:nofail) if no_fail.nil?
|
102
102
|
no_wait = options.delete(:no_wait)
|
103
|
+
xvfb = options.delete(:xvfb)
|
103
104
|
|
104
105
|
dont_close_in = options.delete(:dont_close_in)
|
105
106
|
|
@@ -117,6 +118,14 @@ module CMD
|
|
117
118
|
|
118
119
|
end
|
119
120
|
|
121
|
+
case xvfb
|
122
|
+
when TrueClass
|
123
|
+
cmd = "xvfb-run --server-args='-screen 0 1024x768x24' --auto-servernum #{cmd}"
|
124
|
+
when String
|
125
|
+
cmd = "xvfb-run --server-args='#{xvfb}' --auto-servernum --server-num=1 #{cmd}"
|
126
|
+
when String
|
127
|
+
end
|
128
|
+
|
120
129
|
if stderr == true
|
121
130
|
stderr = Log::HIGH
|
122
131
|
end
|
@@ -287,15 +287,21 @@ module Misc
|
|
287
287
|
when Symbol
|
288
288
|
obj.to_s
|
289
289
|
when (defined?(Path) and Path)
|
290
|
-
if
|
291
|
-
|
292
|
-
|
293
|
-
|
290
|
+
if defined?(Step) && Open.exists?(Step.info_file(obj))
|
291
|
+
obj2str(Workflow.load_step(obj))
|
292
|
+
elsif defined?(Step) && Step === obj.resource
|
293
|
+
"Step file: " + obj
|
294
|
+
else
|
295
|
+
if obj.exists?
|
296
|
+
if obj.directory?
|
297
|
+
files = obj.glob("**/*")
|
298
|
+
"directory: #{Misc.fingerprint(files)}"
|
299
|
+
else
|
300
|
+
"file: " << Open.realpath(obj) << "--" << mtime_str(obj)
|
301
|
+
end
|
294
302
|
else
|
295
|
-
|
303
|
+
obj + " (file missing)"
|
296
304
|
end
|
297
|
-
else
|
298
|
-
obj + " (file missing)"
|
299
305
|
end
|
300
306
|
when String
|
301
307
|
if Misc.is_filename?(obj) and ! %w(. ..).include?(obj)
|
@@ -318,7 +324,11 @@ module Misc
|
|
318
324
|
remove_long_items(obj)
|
319
325
|
when File
|
320
326
|
if obj.respond_to? :filename and obj.filename
|
321
|
-
|
327
|
+
if defined?(Step) && Open.exists?(Step.info_file(obj.filename))
|
328
|
+
obj2str(Workflow.load_step(obj.filename))
|
329
|
+
else
|
330
|
+
"<IO:" << obj.filename << "--" << mtime_str(obj.filename) << ">"
|
331
|
+
end
|
322
332
|
else
|
323
333
|
"<IO:" << obj.path << "--" << mtime_str(obj.path) << ">"
|
324
334
|
end
|
@@ -326,7 +336,11 @@ module Misc
|
|
326
336
|
"<IO:" << obj.short_path << ">"
|
327
337
|
when IO
|
328
338
|
if obj.respond_to? :filename and obj.filename
|
329
|
-
|
339
|
+
if defined?(Step) && Open.exists?(Step.info_file(obj.filename))
|
340
|
+
obj2str(Workflow.load_step(obj.filename))
|
341
|
+
else
|
342
|
+
"<IO:" << obj.filename << "--" << mtime_str(obj.filename) << ">"
|
343
|
+
end
|
330
344
|
else
|
331
345
|
|
332
346
|
if obj.respond_to? :obj2str
|
data/lib/rbbt/workflow.rb
CHANGED
@@ -385,7 +385,7 @@ module Workflow
|
|
385
385
|
next if default == v
|
386
386
|
next if (String === default and Symbol === v and v.to_s == default)
|
387
387
|
next if (Symbol === default and String === v and v == default.to_s)
|
388
|
-
real_inputs[k] = v
|
388
|
+
real_inputs[k.to_sym] = v
|
389
389
|
end
|
390
390
|
|
391
391
|
jobname_input_value = inputs[jobname_input] || all_defaults[jobname_input]
|
@@ -410,6 +410,7 @@ module Workflow
|
|
410
410
|
job.workflow = self
|
411
411
|
job.clean_name = jobname
|
412
412
|
job.overriden = overriden
|
413
|
+
job.real_inputs = real_inputs.keys
|
413
414
|
job
|
414
415
|
end
|
415
416
|
|
@@ -16,6 +16,10 @@ end
|
|
16
16
|
|
17
17
|
module Workflow
|
18
18
|
|
19
|
+
def self.job_path?(path)
|
20
|
+
path.split("/")[-4] == "jobs"
|
21
|
+
end
|
22
|
+
|
19
23
|
def log(status, message = nil, &block)
|
20
24
|
Step.log(status, message, nil, &block)
|
21
25
|
end
|
@@ -301,7 +305,9 @@ module Workflow
|
|
301
305
|
|
302
306
|
def setup_override_dependency(dep, workflow, task_name)
|
303
307
|
dep = Step === dep ? dep : Workflow.load_step(dep)
|
308
|
+
dep.workflow = workflow
|
304
309
|
dep.info[:name] = dep.name
|
310
|
+
dep.original_task_name ||= dep.task_name if dep.workflow
|
305
311
|
begin
|
306
312
|
workflow = Kernel.const_get workflow if String === workflow
|
307
313
|
dep.task = workflow.tasks[task_name] if dep.task.nil? && workflow.tasks.include?(task_name)
|
@@ -309,7 +315,7 @@ module Workflow
|
|
309
315
|
Log.exception $!
|
310
316
|
end
|
311
317
|
dep.task_name = task_name
|
312
|
-
dep.overriden =
|
318
|
+
dep.overriden = dep.original_task_name.to_sym
|
313
319
|
dep
|
314
320
|
end
|
315
321
|
|
@@ -360,7 +366,7 @@ module Workflow
|
|
360
366
|
compute = options[:compute]
|
361
367
|
|
362
368
|
options = IndiferentHash.setup(options.dup)
|
363
|
-
dep = dependency.call jobname,
|
369
|
+
dep = dependency.call jobname, _inputs.merge(options), real_dependencies
|
364
370
|
|
365
371
|
dep = [dep] unless Array === dep
|
366
372
|
|
@@ -77,6 +77,7 @@ module Workflow
|
|
77
77
|
task name do
|
78
78
|
raise RbbtException, "dependency not found in dep_task" if dependencies.empty?
|
79
79
|
dep = dependencies.last.join
|
80
|
+
raise dep.get_exception if dep.error?
|
80
81
|
set_info :result_type, dep.info[:result_type]
|
81
82
|
forget = config :forget_dep_tasks, :forget_dep_tasks, :default => FORGET_DEP_TASKS
|
82
83
|
if forget
|
@@ -50,8 +50,8 @@ module Workflow
|
|
50
50
|
case input_types[input]
|
51
51
|
when :file
|
52
52
|
Log.debug "Pointing #{ input } to #{file}"
|
53
|
-
if file =~ /\.
|
54
|
-
inputs[input.to_sym] = Open.read(file)
|
53
|
+
if file =~ /\.yaml/
|
54
|
+
inputs[input.to_sym] = YAML.load(Open.read(file))
|
55
55
|
else
|
56
56
|
inputs[input.to_sym] = Open.realpath(file)
|
57
57
|
end
|
data/lib/rbbt/workflow/step.rb
CHANGED
@@ -12,6 +12,9 @@ class Step
|
|
12
12
|
attr_accessor :exec
|
13
13
|
attr_accessor :relocated
|
14
14
|
attr_accessor :result, :mutex, :seen
|
15
|
+
attr_accessor :real_inputs, :original_task_name
|
16
|
+
|
17
|
+
RBBT_DEBUG_CLEAN = ENV["RBBT_DEBUG_CLEAN"] == 'true'
|
15
18
|
|
16
19
|
class << self
|
17
20
|
attr_accessor :lock_dir
|
@@ -143,11 +146,13 @@ class Step
|
|
143
146
|
seen = []
|
144
147
|
while path = deps.pop
|
145
148
|
dep_info = archived_info[path]
|
146
|
-
dep_info
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
149
|
+
if dep_info
|
150
|
+
dep_info[:inputs].each do |k,v|
|
151
|
+
all_inputs[k] = v unless all_inputs.include?(k)
|
152
|
+
end if dep_info[:inputs]
|
153
|
+
deps.concat(dep_info[:dependencies].collect{|p| p.last } - seen) if dep_info[:dependencies]
|
154
|
+
deps.concat(dep_info[:archived_dependencies].collect{|p| p.last } - seen) if dep_info[:archived_dependencies]
|
155
|
+
end
|
151
156
|
seen << path
|
152
157
|
end
|
153
158
|
|
@@ -156,7 +161,7 @@ class Step
|
|
156
161
|
|
157
162
|
def dependencies=(dependencies)
|
158
163
|
@dependencies = dependencies
|
159
|
-
set_info :dependencies, dependencies.collect{|dep| [dep.task_name, dep.name, dep.path]}
|
164
|
+
set_info :dependencies, dependencies.collect{|dep| [dep.task_name, dep.name, dep.path]} if dependencies
|
160
165
|
end
|
161
166
|
|
162
167
|
def recursive_inputs
|
@@ -454,6 +459,7 @@ class Step
|
|
454
459
|
status << "not running" if ! done? && ! running?
|
455
460
|
status.unshift " " if status.any?
|
456
461
|
Log.high "Cleaning step: #{path}#{status * " "}"
|
462
|
+
Log.stack caller if RBBT_DEBUG_CLEAN
|
457
463
|
abort if ! done? && running?
|
458
464
|
Step.clean(path)
|
459
465
|
self
|
@@ -8,6 +8,16 @@ class Step
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
+
def self.serialize_info(info)
|
12
|
+
info = info.clean_version if IndiferentHash === info
|
13
|
+
INFO_SERIALIZER.dump(info)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.load_serialized_info(io)
|
17
|
+
IndiferentHash.setup(INFO_SERIALIZER.load(io))
|
18
|
+
end
|
19
|
+
|
20
|
+
|
11
21
|
def self.wait_for_jobs(jobs)
|
12
22
|
jobs = [jobs] if Step === jobs
|
13
23
|
begin
|
@@ -59,7 +69,7 @@ class Step
|
|
59
69
|
def self.step_info(path)
|
60
70
|
begin
|
61
71
|
Open.open(info_file(path), :mode => 'rb') do |f|
|
62
|
-
|
72
|
+
self.load_serialized_info(f)
|
63
73
|
end
|
64
74
|
rescue Exception
|
65
75
|
Log.exception $!
|
@@ -83,18 +93,22 @@ class Step
|
|
83
93
|
|
84
94
|
Log.debug "Saving job input #{name} (#{type}) into #{path}"
|
85
95
|
case
|
96
|
+
when Step === value
|
97
|
+
Open.ln_s(value.path, path)
|
98
|
+
when type.to_s == "file"
|
99
|
+
if String === value && File.exists?(value)
|
100
|
+
Open.ln_s(value, path)
|
101
|
+
else
|
102
|
+
Open.write(path + '.yaml', value.to_yaml)
|
103
|
+
end
|
86
104
|
when Array === value
|
87
|
-
Open.write(path, value * "\n")
|
105
|
+
Open.write(path, value.collect{|v| Step === v ? v.path : v.to_s} * "\n")
|
88
106
|
when IO === value
|
89
|
-
|
90
|
-
|
91
|
-
if String === value && File.exists?(value)
|
92
|
-
Open.link(value, path)
|
107
|
+
if value.filename && String === value.filename && File.exists?(value.filename)
|
108
|
+
Open.ln_s(value.filename, path)
|
93
109
|
else
|
94
|
-
Open.write(path
|
110
|
+
Open.write(path, value)
|
95
111
|
end
|
96
|
-
when Step === value
|
97
|
-
value = value.produce.load
|
98
112
|
else
|
99
113
|
Open.write(path, value.to_s)
|
100
114
|
end
|
@@ -104,18 +118,24 @@ class Step
|
|
104
118
|
def self.save_job_inputs(job, dir, options = nil)
|
105
119
|
options = IndiferentHash.setup options.dup if options
|
106
120
|
|
107
|
-
task_name = job.task_name
|
121
|
+
task_name = Symbol === job.overriden ? job.overriden : job.task_name
|
108
122
|
workflow = job.workflow
|
109
123
|
workflow = Kernel.const_get workflow if String === workflow
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
124
|
+
if workflow
|
125
|
+
task_info = workflow.task_info(task_name)
|
126
|
+
input_types = task_info[:input_types]
|
127
|
+
task_inputs = task_info[:inputs]
|
128
|
+
input_defaults = task_info[:input_defaults]
|
129
|
+
else
|
130
|
+
task_info = input_types = task_inputs = input_defaults = {}
|
131
|
+
end
|
114
132
|
|
115
133
|
inputs = {}
|
134
|
+
real_inputs = job.real_inputs || job.info[:real_inputs]
|
116
135
|
job.recursive_inputs.zip(job.recursive_inputs.fields).each do |value,name|
|
117
136
|
next unless task_inputs.include? name.to_sym
|
118
|
-
next
|
137
|
+
next unless real_inputs.include? name.to_sym
|
138
|
+
next if options && ! options.include?(name)
|
119
139
|
next if value.nil?
|
120
140
|
next if input_defaults[name] == value
|
121
141
|
inputs[name] = value
|
@@ -188,7 +208,7 @@ class Step
|
|
188
208
|
info_lock.lock if check_lock and false
|
189
209
|
begin
|
190
210
|
Open.open(info_file, :mode => 'rb') do |file|
|
191
|
-
|
211
|
+
Step.load_serialized_info(file)
|
192
212
|
end
|
193
213
|
ensure
|
194
214
|
info_lock.unlock if check_lock and false
|
@@ -204,7 +224,7 @@ class Step
|
|
204
224
|
Log.debug{"Error loading info file: " + info_file}
|
205
225
|
Log.exception $!
|
206
226
|
Open.rm info_file
|
207
|
-
Misc.sensiblewrite(info_file,
|
227
|
+
Misc.sensiblewrite(info_file, Step.serialize_info({:status => :error, :messages => ["Info file lost"]}))
|
208
228
|
raise $!
|
209
229
|
end
|
210
230
|
end
|
@@ -212,10 +232,10 @@ class Step
|
|
212
232
|
def init_info(force = false)
|
213
233
|
return nil if @exec || info_file.nil? || (Open.exists?(info_file) && ! force)
|
214
234
|
Open.lock(info_file, :lock => info_lock) do
|
215
|
-
i = {:status => :waiting, :pid => Process.pid, :path => path}
|
235
|
+
i = {:status => :waiting, :pid => Process.pid, :path => path, :real_inputs => real_inputs}
|
216
236
|
i[:dependencies] = dependencies.collect{|dep| [dep.task_name, dep.name, dep.path]} if dependencies
|
217
|
-
|
218
|
-
|
237
|
+
Misc.sensiblewrite(info_file, Step.serialize_info(i), :force => true, :lock => false)
|
238
|
+
@info_cache = IndiferentHash.setup(i)
|
219
239
|
@info_cache_time = Time.now
|
220
240
|
end
|
221
241
|
end
|
@@ -227,9 +247,9 @@ class Step
|
|
227
247
|
Open.lock(info_file, :lock => info_lock) do
|
228
248
|
i = info(false).dup
|
229
249
|
i[key] = value
|
230
|
-
|
231
|
-
|
232
|
-
Misc.sensiblewrite(info_file, dump, :force => true, :lock => false)
|
250
|
+
dump = Step.serialize_info(i)
|
251
|
+
@info_cache = IndiferentHash.setup(i)
|
252
|
+
Misc.sensiblewrite(info_file, dump, :force => true, :lock => false) if Open.exists?(info_file)
|
233
253
|
@info_cache_time = Time.now
|
234
254
|
value
|
235
255
|
end
|
@@ -242,9 +262,9 @@ class Step
|
|
242
262
|
Open.lock(info_file, :lock => info_lock) do
|
243
263
|
i = info(false)
|
244
264
|
i.merge! hash
|
245
|
-
|
246
|
-
|
247
|
-
Misc.sensiblewrite(info_file, dump, :force => true, :lock => false)
|
265
|
+
dump = Step.serialize_info(i)
|
266
|
+
@info_cache = IndiferentHash.setup(i)
|
267
|
+
Misc.sensiblewrite(info_file, dump, :force => true, :lock => false) if Open.exists?(info_file)
|
248
268
|
@info_cache_time = Time.now
|
249
269
|
value
|
250
270
|
end
|
@@ -537,7 +557,7 @@ class Step
|
|
537
557
|
end
|
538
558
|
|
539
559
|
def file(name)
|
540
|
-
Path.setup(File.join(files_dir, name.to_s))
|
560
|
+
Path.setup(File.join(files_dir, name.to_s), workflow, self)
|
541
561
|
end
|
542
562
|
|
543
563
|
def save_file(name, content)
|