rack-webdav 0.4.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 56d779afdbd6da5c459d33219b508ed4b141a564
4
+ data.tar.gz: 1bba827edea7b0d09f7b22ecb013660966c23ee8
5
+ SHA512:
6
+ metadata.gz: 888809b2b3deebd298ab26810d980686df9cf912ef3f8d557de2e5dfd45a3d0a3fa110dcd8562c4c3c38e6df1137901128eba54021e431139352e1cf0ba1e954
7
+ data.tar.gz: b545e17b78a6ec5c5c8ff1ae9e6d7cb4e682c152ed1d484ca8b5dd92a6be6599f0b4b68bd23bb72e84ae8013724ea2d280f9800ccc329369eea3ee5762bb2e4a
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ .*
2
+ !.gitignore
3
+ *.gem
4
+ *~
5
+ doc/*
6
+ pkg/*
7
+ test/repo
8
+ /vendor/bundle
data/LICENSE ADDED
@@ -0,0 +1,42 @@
1
+ Current RackWebDAV license:
2
+
3
+ Copyright (c) 2010 Chris Roberts <chrisroberts.code@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to
7
+ deal in the Software without restriction, including without limitation the
8
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9
+ sell copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+
23
+ Original rack_dav source license:
24
+
25
+ Copyright (c) 2009 Matthias Georgi <http://www.matthias-georgi.de>
26
+
27
+ Permission is hereby granted, free of charge, to any person obtaining a copy
28
+ of this software and associated documentation files (the "Software"), to
29
+ deal in the Software without restriction, including without limitation the
30
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
31
+ sell copies of the Software, and to permit persons to whom the Software is
32
+ furnished to do so, subject to the following conditions:
33
+
34
+ The above copyright notice and this permission notice shall be included in
35
+ all copies or substantial portions of the Software.
36
+
37
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
40
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
41
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
42
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,336 @@
1
+ = RackWebDAV - Web Authoring for Rack
2
+
3
+ RackWebDAV based by (dav4rack)[https://github.com/chrisroberts/dav4rack]
4
+
5
+ == Motivaion
6
+
7
+ * dav4rack no maintained
8
+ * I want to implment the following functions
9
+ * WebDAV RFC 3253 ( https://www.ietf.org/rfc/rfc3253.txt )
10
+ * because support Subversion (SVN)
11
+ * Rails 5 support
12
+ * Rack 2.0 support
13
+
14
+ == Current Feature
15
+
16
+ === From DAV4Rack
17
+ * Better resource support for building fully virtualized resource structures
18
+ * Generic locking as well as Resource level specific locking
19
+ * Interceptor middleware to provide virtual mapping to resources
20
+ * Mapped resource paths
21
+ * Authentication support
22
+ * Resource callbacks
23
+ * Remote file proxying (including sendfile support for remote files)
24
+
25
+ === RackWebDAV original
26
+ * Rack 2.0 support
27
+ * serveral bugfix
28
+
29
+ == Install
30
+ === RubyGems
31
+
32
+ gem install rack-webdav
33
+
34
+ == Documentation
35
+
36
+ * TODO
37
+
38
+ == Quickstart
39
+
40
+ If you just want to share a folder over WebDAV, you can just start a
41
+ simple server with:
42
+
43
+ rack-webdav
44
+
45
+ This will start a Unicorn, Mongrel or WEBrick server on port 3000, which you can connect
46
+ to without authentication. Unicorn and Mongrel will be much more responsive than WEBrick,
47
+ so if you are having slowness issues, install one of them and restart the rack-webdav process.
48
+ The simple file resource allows very basic authentication which is used for an example. To enable it:
49
+
50
+ rack-webdav --username=user --password=pass
51
+
52
+ == Rack Handler
53
+
54
+ Using RackWebDAV within a rack application is pretty simple. A very slim
55
+ rackup script would look something like this:
56
+
57
+ require 'rubygems'
58
+ require 'rack-webdav'
59
+
60
+ use Rack::CommonLogger
61
+ run RackWebDAV::Handler.new(:root => '/path/to/public/fileshare')
62
+
63
+ This will use the included FileResource and set the share path. However,
64
+ RackWebDAV has some nifty little extras that can be enabled in the rackup script. First,
65
+ an example of how to use a custom resource:
66
+
67
+ run RackWebDAV::Handler.new(:resource_class => CustomResource, :custom => 'options', :passed => 'to resource')
68
+
69
+ Next, lets venture into mapping a path for our WebDAV access. In this example, we
70
+ will use default FileResource like in the first example, but instead of the WebDAV content
71
+ being available at the root directory, we will map it to a specific directory: /webdav/share/
72
+
73
+ require 'rubygems'
74
+ require 'rack-webdav'
75
+
76
+ use Rack::CommonLogger
77
+
78
+ app = Rack::Builder.new{
79
+ map '/webdav/share/' do
80
+ run RackWebDAV::Handler.new(:root => '/path/to/public/fileshare', :root_uri_path => '/webdav/share/')
81
+ end
82
+ }.to_app
83
+ run app
84
+
85
+ Aside from the Builder#map block, notice the new option passed to the Handler's initialization, :root_uri_path. When
86
+ RackWebDAV receives a request, it will automatically convert the request to the proper path and pass it to
87
+ the resource.
88
+
89
+ Another tool available when building the rackup script is the Interceptor. The Interceptor's job is to simply
90
+ intecept WebDAV requests received up the path hierarchy where no resources are currently mapped. For example,
91
+ lets continue with the last example but this time include the interceptor:
92
+
93
+ require 'rubygems'
94
+ require 'rack-webdav'
95
+
96
+ use Rack::CommonLogger
97
+ app = Rack::Builder.new{
98
+ map '/webdav/share/' do
99
+ run RackWebDAV::Handler.new(:root => '/path/to/public/fileshare', :root_uri_path => '/webdav/share/')
100
+ end
101
+ map '/webdav/share2/' do
102
+ run RackWebDAV::Handler.new(:resource_class => CustomResource, :root_uri_path => '/webdav/share2/')
103
+ end
104
+ map '/' do
105
+ use RackWebDAV::Interceptor, :mappings => {
106
+ '/webdav/share/' => {:resource_class => FileResource, :custom => 'option'},
107
+ '/webdav/share2/' => {:resource_class => CustomResource}
108
+ }
109
+ use Rails::Rack::Static
110
+ run ActionController::Dispatcher.new
111
+ end
112
+ }.to_app
113
+ run app
114
+
115
+ In this example we have two WebDAV resources restricted by path. This means those resources will handle requests to /webdav/share/*
116
+ and /webdav/share2/* but nothing above that. To allow webdav to respond, we provide the Interceptor. The Interceptor does not
117
+ provide any authentication support. It simply creates a virtual file system view to the provided mapped paths. Once the actual
118
+ resources have been reached, authentication will be enforced based on the requirements defined by the individual resource. Also
119
+ note in the root map you can see we are running a Rails application. This is how you can easily enable RackWebDAV with your Rails
120
+ application.
121
+
122
+ === Enabling Logging
123
+
124
+ RackWebDAV provides some simple logging in a Rails style format (simply for consistency) so the output should look some what familiar.
125
+
126
+ RackWebDAV::Handler.new(:resource_class => CustomResource, :log_to => '/my/log/file')
127
+
128
+ You can even specify the level of logging:
129
+
130
+ RackWebDAV::Handler.new(:resource_class => CustomResource, :log_to => ['/my/log/file', Logger::DEBUG])
131
+
132
+ == Custom Resources
133
+
134
+ Creating your own resource is easy. Simply inherit the RackWebDAV::Resource class, and start redefining all the methods
135
+ you want to customize. The RackWebDAV::Resource class only has implementations for methods that can be provided extremely
136
+ generically. This means that most things will require at least some sort of implementation. However, because the Resource
137
+ is defined so generically, and the Controller simply passes the request on to the Resource, it is easy to create fully
138
+ virtualized resources.
139
+
140
+ == Helpers
141
+
142
+ There are some helpers worth mentioning that make things a little easier. RackWebDAV::Resource#accept_redirect? method is available to Resources.
143
+ If true, the currently connected client will accept and properly use a 302 redirect for a GET request. Most clients do not properly
144
+ support this, which can be a real pain when working with virtualized files that may be located some where else, like S3. To deal with
145
+ those clients that don't support redirects, a helper has been provided so resources don't have to deal with proxying themselves. The RackWebDAV::RemoteFile
146
+ is a modified Rack::File that can do some interesting things. First, lets look at its most basic use:
147
+
148
+ class MyResource < RackWebDAV::Resource
149
+ def setup
150
+ @item = method_to_fill_this_properly
151
+ end
152
+
153
+ def get
154
+ if(accept_redirect?)
155
+ response.redirect item[:url]
156
+ else
157
+ response.body = RackWebDAV::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type)
158
+ OK
159
+ end
160
+ end
161
+ end
162
+
163
+ This is a simple proxy. When Rack receives the RemoteFile, it will pull a chunk of data from object, which in turn pulls it from the socket, and
164
+ sends it to the user over and over again until the EOF is reached. This much the same method that Rack::File uses but instead we are pulling
165
+ from a socket rather than an actual file. Now, instead of proxying these files from a remote server every time, lets cache them:
166
+
167
+ response.body = RackWebDAV::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :cache_directory => '/tmp')
168
+
169
+ Providing the :cache_directory will let RemoteFile cache the items locally, and then search for them on subsequent requests before heading out
170
+ to the network. The cached file name is based off the SHA1 hash of the file path, size and last modified time. It is important to note that for
171
+ services like S3, the path will often change, making this cache pretty worthless. To combat this, we can provide a reference to use instead:
172
+
173
+ response.body = RackWebDAV::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :cache_directory => '/tmp', :cache_ref => item[:static_url])
174
+
175
+ These methods will work just fine, but it would be really nice to just let someone else deal with the proxying and let the process get back
176
+ to dealing with actual requests. RemoteFile will happily do that as long as the frontend server is setup correctly. Using the sendfile approach
177
+ will tell the RemoteFile to simply pass the headers on and let the server deal with doing the actual proxying. First, lets look at an implementation
178
+ using all the features, and then degrade that down to the bare minimum. These examples are NGINX specific, but are based off the Rack::Sendfile implementation
179
+ and as such should be applicable to other servers. First, a simplified NGINX server block:
180
+
181
+ server {
182
+ listen 80;
183
+ location / {
184
+ proxy_set_header X-Real-IP $remote_addr;
185
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
186
+ proxy_set_header Host $http_host;
187
+ proxy_set_header X-Sendfile-Type X-Accel-Redirect;
188
+ proxy_set_header X-Accel-Remote-Mapping webdav_redirect
189
+ proxy_pass http://my_app_server;
190
+ }
191
+
192
+ location ~* /webdav_redirect {
193
+ internal;
194
+ resolver 127.0.0.1;
195
+ set $r_host $upstream_http_redirect_host;
196
+ set $r_url $upstream_http_redirect_url;
197
+ proxy_set_header Authorization '';
198
+ proxy_set_header Host $r_host;
199
+ proxy_max_temp_file_size 0;
200
+ proxy_pass $r_url;
201
+ }
202
+ }
203
+
204
+ With this in place, the parameters for the RemoteFile change slightly:
205
+
206
+ response.body = RackWebDAV::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :sendfile => true)
207
+
208
+ The RemoteFile will automatically take care of building out the correct path and sending the proper headers. If the X-Accel-Remote-Mapping header
209
+ is not available, you can simply pass the value:
210
+
211
+ response.body = RackWebDAV::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :sendfile => true, :sendfile_prefix => 'webdav_redirect')
212
+
213
+ And if you don't have the X-Sendfile-Type header set, you can fix that by changing the value of :sendfile:
214
+
215
+ response.body = RackWebDAV::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :sendfile => 'X-Accel-Redirect', :sendfile_prefix => 'webdav_redirect')
216
+
217
+ And if you have none of the above because your server hasn't been configured for sendfile support, you're out of luck until it's configured.
218
+
219
+ == Authentication
220
+
221
+ Authentication is performed on a per Resource basis. The Controller object will check the Resource for a Resource#authenticate method. If it exists,
222
+ any authentication information will be passed to the method. Depending on the result, the Controller will either continue on with the request, or send
223
+ a 401 Unauthorized response.
224
+
225
+ As a nicety, Resource#authentication_realm will be checked for existence and the returning string will be used as the Realm. Resource#authentication_error_msg
226
+ will also be checked for existence and the returning string will be passed in the response upon authentication failure.
227
+
228
+ Authentication can also be implemented using callbacks, as discussed below.
229
+
230
+ == Callbacks
231
+
232
+ Resources can make use of callbacks to easily apply permissions, authentication or any other action that needs to be performed before or after any or all
233
+ actions. Callbacks are applied to all publicly available methods. This is important for methods used internally within the resource. Methods not meant
234
+ to be called by the Controller, or anyone else, should be scoped protected or private to reduce the interaction with callbacks.
235
+
236
+ Callbacks can be called before or after a method call. For example:
237
+
238
+ class MyResource < RackWebDAV::Resource
239
+ before do |resource, method_name|
240
+ resource.send(:my_authentication_method)
241
+ end
242
+
243
+ after do |resource, method_name|
244
+ puts "#{Time.now} -> Completed: #{resource}##{method_name}"
245
+ end
246
+
247
+ private
248
+
249
+ def my_authentication_method
250
+ true
251
+ end
252
+ end
253
+
254
+ In this example MyResource#my_authentication_method will be called before any public method is called. After any method has been called a status
255
+ line will be printed to STDOUT. Running callbacks before/after every method call is a bit much in most cases, so callbacks can be applied to specific
256
+ methods:
257
+
258
+ class MyResource < RackWebDAV::Resource
259
+ before_get do |resource|
260
+ puts "#{Time.now} -> Received GET request from resource: #{resource}"
261
+ end
262
+ end
263
+
264
+ In this example, a simple status line will be printed to STDOUT before the MyResource#get method is called. The current resource object is always
265
+ provided to callbacks. The method name is only provided to the generic before/after callbacks.
266
+
267
+ Something very handy for dealing with the mess of files OS X leaves on the system:
268
+
269
+ class MyResource < RackWebDAV::Resource
270
+ after_unlock do |resource|
271
+ resource.delete if resource.name[0,1] == '.'
272
+ end
273
+ end
274
+
275
+ Because OS X implements locking correctly, we can wait until it releases the lock on the file, and remove it if it's a hidden file.
276
+
277
+ Callbacks are called in the order they are defined, so you can easily build callbacks off each other. Like this example:
278
+
279
+ class MyResource < RackWebDAV::Resource
280
+ before do |resource, method_name|
281
+ resource.DAV_authenticate unless resource.user.is_a?(User)
282
+ raise Unauthorized unless resource.user.is_a?(User)
283
+ end
284
+ before do |resource, method_name|
285
+ resource.user.allowed?(method_name)
286
+ end
287
+ end
288
+
289
+ In this example, the second block checking User#allowed? can count on Resource#user being defined because the blocks are called in
290
+ order, and if the Resource#user is not a User type, an exception is raised.
291
+
292
+ === Avoiding callbacks
293
+
294
+ Something special to notice in the last example is the DAV_ prefix on authenticate. Providing the DAV_ prefix will prevent
295
+ any callbacks being applied to the given method. This allows us to provide a public method that the callback can access on the resource
296
+ without getting stuck in a loop.
297
+
298
+ == Software using RackWebDAV!
299
+
300
+ * {meishi}[https://github.com/inferiorhumanorgans/meishi] - Lightweight CardDAV implementation in Rails
301
+ * {rack-webdav_ext}[https://github.com/schmurfy/rack-webdav_ext] - CardDAV extension. (CalDAV planned)
302
+
303
+ == Issues/Bugs/Questions
304
+
305
+ === Known Issues
306
+
307
+ * OS X Finder PUT fails when using NGINX (this is due to NGINX's lack of chunked transfer encoding)
308
+ * Windows WebDAV mini-redirector fails (this client is very broken. patches welcome.)
309
+ * Lots of unimplemented parts of the webdav spec (patches always welcome)
310
+
311
+ === Unknown Issues
312
+
313
+ Please use the issues at github: http://github.com/chrisroberts/rack-webdav/issues
314
+
315
+ == Contributors
316
+
317
+ A big thanks to everyone contributing to help make this project better.
318
+
319
+ * {clyfe}[http://github.com/clyfe]
320
+ * {antiloopgmbh}[http://github.com/antiloopgmbh]
321
+ * {krug}[http://github.com/krug]
322
+ * {teefax}[http://github.com/teefax]
323
+ * {buffym}[https://github.com/buffym]
324
+ * {jbangert}[https://github.com/jbangert]
325
+ * {doxavore}[https://github.com/doxavore]
326
+ * {spicyj}[https://github.com/spicyj]
327
+ * {TurchenkoAlex}[https://github.com/TurchenkoAlex]
328
+ * {exabugs}[https://github.com/exabugs]
329
+ * {inferiorhumanorgans}[https://github.com/inferiorhumanorgans]
330
+ * {schmurfy}[https://github.com/schmurfy]
331
+ * {pifleo}[https://github.com/pifleo]
332
+ * {mlmorg}[https://github.com/mlmorg]
333
+
334
+ == License
335
+
336
+ Just like RackDAV before it, this software is distributed under the MIT license.
data/bin/rack-webdav ADDED
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'rack-webdav/version'
5
+ require 'getoptlong'
6
+
7
+ def print_help_msg
8
+ print_version_info
9
+ puts "Usage: rack-webdav [opts]"
10
+ puts " --help Print help message"
11
+ puts " --version Print version information"
12
+ puts " --username name Set username"
13
+ puts " --password pass Set password"
14
+ puts " --root /share/path Set path to share directory"
15
+ puts " --log /path/to/log Set path for logging file"
16
+ puts " --verbosity opt Set logging verbosity. (Valid: debug,info,warn,fatal)"
17
+ end
18
+
19
+ def print_version_info
20
+ puts "DAV 4 Rack - Rack based WebDAV Framework - Version: #{RackWebDAV::VERSION}"
21
+ end
22
+
23
+ opts = GetoptLong.new(
24
+ ['--username', '-u', GetoptLong::REQUIRED_ARGUMENT],
25
+ ['--password', '-p', GetoptLong::REQUIRED_ARGUMENT],
26
+ ['--help', '-h', GetoptLong::NO_ARGUMENT],
27
+ ['--version', '-v', GetoptLong::NO_ARGUMENT],
28
+ ['--root', '-r', GetoptLong::REQUIRED_ARGUMENT],
29
+ ['--log', '-l', GetoptLong::OPTIONAL_ARGUMENT],
30
+ ['--verbosity', '-V', GetoptLong::REQUIRED_ARGUMENT],
31
+ ['--pretty-xml', '-P', GetoptLong::REQUIRED_ARGUMENT]
32
+ )
33
+
34
+ credentials = {}
35
+
36
+ opts.each do |opt,arg|
37
+ case opt
38
+ when '--help'
39
+ print_help_msg
40
+ exit(0)
41
+ when '--username'
42
+ credentials[:username] = arg
43
+ when '--password'
44
+ credentials[:password] = arg
45
+ when '--root'
46
+ credentials[:root] = arg
47
+ unless(File.exists?(arg) && File.directory?(arg))
48
+ puts "ERROR: Path provided is not a valid directory (#{arg})"
49
+ exit(-1)
50
+ end
51
+ when '--pretty-xml'
52
+ credentials[:pretty_xml] = true
53
+ when '--version'
54
+ print_version_info
55
+ exit(0)
56
+ when '--log'
57
+ require 'pathname'
58
+ require 'logger'
59
+ credentials[:log_to] = [nil, Logger::FATAL] unless credentials[:log_to]
60
+ if(arg && !arg.empty?)
61
+ dirname = Pathname.new(arg).dirname
62
+ if((File.exists?(arg) && File.writable?(arg)) || (File.exists?(dirname) && File.writable?(dirname)))
63
+ credentials[:log_to][0] = arg
64
+ else
65
+ puts "ERROR: Log file is not writable: #{arg}"
66
+ exit(-1)
67
+ end
68
+ else
69
+ credentials[:log_to][0] = $stdout
70
+ end
71
+ when '--verbosity'
72
+ require 'logger'
73
+ begin
74
+ credentials[:log_to] = [] unless credentials[:log_to]
75
+ credentials[:log_to][1] = Logger.const_get(arg.upcase)
76
+ rescue NameError
77
+ puts "ERROR: Unknown verbosity level given: #{arg}"
78
+ exit(-1)
79
+ end
80
+ else
81
+ puts "ERROR: Unknown option provided"
82
+ exit(-1)
83
+ end
84
+ end
85
+
86
+ require 'rack-webdav'
87
+
88
+ app = Rack::Builder.new do
89
+ use Rack::ShowExceptions
90
+ use Rack::CommonLogger
91
+ use Rack::Reloader
92
+ use Rack::Lint
93
+
94
+ run RackWebDAV::Handler.new(credentials)
95
+
96
+ end.to_app
97
+
98
+ runners = []
99
+ runners << lambda do |x|
100
+ print 'Looking for unicorn... '
101
+ require 'unicorn'
102
+ puts 'OK'
103
+ if(Unicorn.respond_to?(:run))
104
+ Unicorn.run(x, :listeners => ["0.0.0.0:3000"])
105
+ else
106
+ Unicorn::HttpServer.new(x, :listeners => ["0.0.0.0:3000"]).start.join
107
+ end
108
+ end
109
+ runners << lambda do |x|
110
+ print 'Looking for mongrel... '
111
+ require 'mongrel'
112
+ puts 'OK'
113
+ Rack::Handler::Mongrel.run(x, :Port => 3000)
114
+ end
115
+ runners << lambda do |x|
116
+ puts 'Loading WEBrick'
117
+ Rack::Handler::WEBrick.run(x, :Port => 3000)
118
+ end
119
+
120
+ begin
121
+ runner = runners.shift
122
+ runner.call(app)
123
+ rescue LoadError
124
+ puts 'FAILED'
125
+ retry unless runners.empty?
126
+ end