omgf 0.0.0.GIT

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ # :stopdoc:
2
+ # Copyright (C) 2008-2012, Eric Wong <normalperson@yhbt.net>
3
+ # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
4
+ # :startdoc:
5
+ module OMGF
6
+ end
@@ -0,0 +1,336 @@
1
+ # -*- encoding: binary -*-
2
+ # :stopdoc:
3
+ # Copyright (C) 2008-2012, Eric Wong <normalperson@yhbt.net>
4
+ # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
5
+ # :startdoc:
6
+ require "omgf/pool"
7
+ require "time"
8
+ require "json"
9
+ require "mogilefs"
10
+ require "rack"
11
+ require "rack/utils"
12
+ require "rack/mime"
13
+
14
+ # Basic REST interface. This is bug-for-bug compatible with an old app
15
+ # that's been deployed for years in a private LAN.
16
+ #
17
+ # This started out as a WORM (write-once, read-many) system, but
18
+ # eventually gained the ability to handle deletes.
19
+ #
20
+ # This was meant to behave like the MogileFS protocol somewhat (but is
21
+ # pure-HTTP), so it redirects to the storage nodes and bypasses Ruby
22
+ # for bulk I/O when retrieving files. PUTs (writes) still go through
23
+ # Ruby, however.
24
+ class OMGF::HystericalRaisins
25
+ include OMGF::Pool
26
+
27
+ def initialize(opts)
28
+ @default_class_cb = opts[:default_class_cb] || {}
29
+ mg_opts = {
30
+ domain: "any",
31
+ hosts: opts[:hosts],
32
+ fail_timeout: opts[:fail_timeout] || 0.5,
33
+
34
+ # high defaults because of slow seeks on storage nodes (for size verify)
35
+ timeout: opts[:timeout] || 30,
36
+ get_file_data_timeout: opts[:get_file_data_timeout] || 30,
37
+ }
38
+ @reproxy_header = opts[:reproxy_header] || "HTTP_X_OMGF_REPROXY"
39
+ @reproxy_path = opts[:reproxy_path]
40
+ @get_paths_opts = {
41
+ noverify: opts[:noverify],
42
+ pathcount: opts[:pathcount] || 0x7fffffff,
43
+ }
44
+ @put_overwrite_header = opts[:put_overwrite_header] || "HTTP_X_OMGF_FORCE"
45
+ pool_init(mg_opts)
46
+ end
47
+
48
+ # The entry point for Rack
49
+ def call(env)
50
+ case env["REQUEST_METHOD"]
51
+ when "GET"
52
+ get(env)
53
+ when "HEAD"
54
+ head(env)
55
+ when "PUT"
56
+ put(env)
57
+ when "DELETE"
58
+ delete(env)
59
+ else
60
+ r(405)
61
+ end
62
+ rescue MogileFS::Backend::UnknownKeyError,
63
+ MogileFS::Backend::DomainNotFoundError
64
+ r(404, "")
65
+ rescue => e
66
+ logger = env["rack.logger"]
67
+ logger.error "#{e.message} (#{e.class})"
68
+ e.backtrace.each { |line| logger.error(line) }
69
+ r(500, "")
70
+ end
71
+
72
+ # GET /$DOMAIN?prefix=foo - list keys
73
+ # GET /$DOMAIN/$KEY - redirects to FIDs on storage nodes
74
+ def get(env)
75
+ case env["PATH_INFO"].squeeze("/")
76
+ when %r{\A/([^/]+)/(.+)\z} # /$DOMAIN/$KEY
77
+ redirect_key(env, $1, $2)
78
+ when %r{\A/([^/]+)/?\z} # /$DOMAIN
79
+ get_keys(env, $1)
80
+ when "/"
81
+ r(200, "")
82
+ else
83
+ r(404, "")
84
+ end
85
+ end
86
+
87
+ # returns metadata for a given domain/key
88
+ def head(env)
89
+ case env["PATH_INFO"].squeeze("/")
90
+ when %r{\A/([^/]+)/(.+)\z} # HEAD /$DOMAIN/$KEY
91
+ stat_key(env, $1, $2)
92
+ else
93
+ # pass on headers from listing results
94
+ status, headers, _ = get(env)
95
+ [ status, headers, [] ]
96
+ end
97
+ end
98
+
99
+ # HEAD /$DOMAIN/$KEY
100
+ def stat_key(env, domain, key)
101
+ size, paths = mg_size_and_paths(domain, key, @get_paths_opts)
102
+ return r(503, "") unless paths && location = paths[0]
103
+ h = { "Content-Length" => size.to_s }
104
+ fn = filename(h, query(env)) || key
105
+ h["Content-Type"] = key_mime_type(fn)
106
+ unless reproxy?(env, key, h, location)
107
+ paths.each_with_index { |path,i| h["X-URL-#{i}"] = path }
108
+ end
109
+ [ 200, h, [] ]
110
+ end
111
+
112
+ # GET /$DOMAIN/$KEY
113
+ def redirect_key(env, domain, key)
114
+ uris = mg_get_uris(domain, key, @get_paths_opts)
115
+
116
+ return r(503, "") unless uris && dest = uris.shift
117
+
118
+ location = dest.to_s
119
+ h = {
120
+ 'Content-Length' => '0',
121
+ 'Location' => location,
122
+ 'Content-Type' => 'text/html'
123
+ }
124
+
125
+ unless reproxy?(env, key, h, location)
126
+ uris -= [ dest ]
127
+ uris.each_with_index { |uri,i| h["X-Alt-Location-#{i}"] = uri.to_s }
128
+ end
129
+ [ 302, h, [] ]
130
+ end
131
+
132
+ # GET /$DOMAIN/
133
+ def get_keys(env, domain) # :nodoc:
134
+ params = query(env)
135
+ prefix = params["prefix"] || ""
136
+ after = params["after"]
137
+ limit = (params["limit"] || 1000).to_i
138
+
139
+ case env["HTTP_ACCEPT"]
140
+ when "application/json"
141
+ tmp = []
142
+ h = { "Content-Type" => "application/json" }
143
+ mg_list_keys(domain, prefix, after, limit) { |*x| tmp << x }
144
+ tmp = tmp.to_json
145
+ else
146
+ tmp = ""
147
+ h = { "Content-Type" => "text/plain" }
148
+ mg_list_keys(domain, prefix, after, limit) do |dkey,length,devcount|
149
+ tmp << "#{dkey}|#{length}|#{devcount}\n"
150
+ end
151
+ end
152
+ h["Content-Length"] = tmp.size.to_s
153
+
154
+ [ 200, h, [ tmp ] ]
155
+ rescue MogileFS::Backend::UnregDomainError,
156
+ MogileFS::Backend::DomainNotFoundError
157
+ r(404, "Domain not found")
158
+ end
159
+
160
+ # returns a plain-text HTTP response
161
+ def r(code, msg = nil, env = nil) # :nodoc:
162
+ if env && logger = env["rack.logger"]
163
+ logger.warn("#{env['REQUEST_METHOD']} #{env['PATH_INFO']} " \
164
+ "#{code} #{msg.inspect}")
165
+ end
166
+
167
+ if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(code)
168
+ [ code, {}, [] ]
169
+ else
170
+ msg ||= Rack::Utils::HTTP_STATUS_CODES[code] || ""
171
+
172
+ if msg.size > 0
173
+ # using += to not modify original string (owned by Rack)
174
+ msg += "\n"
175
+ end
176
+
177
+ [ code,
178
+ { 'Content-Type' => 'text/plain', 'Content-Length' => msg.size.to_s },
179
+ [ msg ] ]
180
+ end
181
+ end
182
+
183
+ # returns the mime type based on key name
184
+ def key_mime_type(key) # :nodoc:
185
+ /\.([^.]+)\z/ =~ key
186
+ Rack::Mime.mime_type($1) # nil => 'application/octet-stream'
187
+ end
188
+
189
+ # sets Content-Disposition response header if requested by the user
190
+ # in a query string (via "inline" or "attachment" param)
191
+ # Returns requested filename, or nil if invalid
192
+ def filename(h, params) # :nodoc:
193
+ if fn = params['inline'] and Rack::Utils.escape(fn) == fn
194
+ h['Content-Disposition'] = "inline; filename=#{fn}"
195
+ return fn
196
+ elsif fn = (params['attachment'] || params['filename']) and
197
+ Rack::Utils.escape(fn) == fn
198
+ h['Content-Disposition'] = "attachment; filename=#{fn}"
199
+ return fn
200
+ end
201
+ end
202
+
203
+ # This relies on nginx to reproxy paths for us
204
+ def reproxy?(env, key, h, location) # :nodoc:
205
+ return false unless @reproxy_path && env[@reproxy_header].to_i != 0
206
+ %r{/0*(\d+)\.fid\z} =~ location
207
+ fid = $1
208
+ h['ETag'] = %("#{fid}")
209
+ fn = filename(h, query(env)) || key
210
+
211
+ case env["REQUEST_METHOD"]
212
+ when "GET"
213
+ # Fake a Last-Modified time (enough to bust caches)
214
+ h['X-Redirect-Last-Modified'] = Time.at(fid.to_i).httpdate
215
+
216
+ # yes we violate Rack::Lint here, no Content-Type or Content-Length
217
+ h.delete("Content-Length")
218
+ h.delete("Content-Type")
219
+ h['X-Redirect-Content-Type'] = key_mime_type(fn)
220
+ h['X-Accel-Redirect'] = @reproxy_path
221
+ when "HEAD"
222
+ # Fake a Last-Modified time (enough to bust caches)
223
+ h['Last-Modified'] = Time.at(fid.to_i).httpdate
224
+ else
225
+ raise "BUG: bad request method #{env["REQUEST_METHOD"]}"
226
+ end
227
+
228
+ true
229
+ end
230
+
231
+ # PUT /domain/key
232
+ def put(env)
233
+ case env["PATH_INFO"]
234
+ when %r{\A/([^/]+)/(.+)\z} # /$DOMAIN/$KEY
235
+ put_key(env, $1, $2)
236
+ else
237
+ r(404, "")
238
+ end
239
+ end
240
+
241
+ # we're very picky about keys, we only allow keys which won't cause
242
+ # ambiguity when passed through URI escapers/normalizers.
243
+ def bad_key?(key) # :nodoc:
244
+ %r{\A[\w./-]+\z} !~ key ||
245
+ %r{//} =~ key ||
246
+ %r{[./]} =~ "#{key[0]}#{key[-1]}"
247
+ end
248
+
249
+ # PUT /$DOMAIN/$KEY
250
+ def put_key(env, domain, key)
251
+ return r(403, "") if env[@reproxy_header].to_i != 0
252
+ return r(406, "key `#{key}' is not URI-friendly") if bad_key?(key)
253
+ return r(406, "key is too long") if key.size > 128
254
+
255
+ # this was written before MogileFS supported empty files,
256
+ # but empty files waste DB space so we don't support them
257
+ # Not bothering with Transfer-Encoding: chunked, though...
258
+ return r(403, "empty files forbidden") if env["CONTENT_LENGTH"] == "0"
259
+
260
+ params = query(env)
261
+ input = env["rack.input"]
262
+ paths = nil
263
+ retried = false
264
+
265
+ begin
266
+ pool_use(domain) do |mg|
267
+ begin
268
+ # TOCTOU issue, but probably not worth worrying about
269
+ # Nothing we can do about it without explicit MogileFS support
270
+ # or a 3rd-party locking daemon
271
+ paths = mg.get_paths(key)
272
+ if paths && paths[0]
273
+
274
+ # overwriting existing files is not permitted by default
275
+ if env[@put_overwrite_header] != "true"
276
+ # show the existing paths in response
277
+ return r(403, paths.join("\n"))
278
+ end
279
+ end
280
+ rescue MogileFS::Backend::UnknownKeyError
281
+ # good, not clobbering anything
282
+ end
283
+
284
+ # the original deployment of this created a class for every
285
+ # domain with the same class having the same name as the domain
286
+ klass = params['class'] || @default_class_cb[domain] || "default"
287
+ mg.store_file(key, klass, input)
288
+ end # pool_use
289
+
290
+ # should always return 201 if ! found, but we keep 200 for legacy
291
+ # compat if they're not logged in (via REMOTE_USER)
292
+ status = paths ? 204 : (env["REMOTE_USER"] ? 201 : 200)
293
+ r(status, "")
294
+ rescue MogileFS::Backend::UnregDomainError,
295
+ MogileFS::Backend::DomainNotFoundError
296
+ r(406, "Invalid domain: #{domain}")
297
+ rescue => e
298
+ if retried == false && input.respond_to?(:rewind)
299
+ begin
300
+ retried = true
301
+ input.rewind # may raise on future perfectly compliant Rack servers
302
+ env["rack.logger"].warn("#{e.message} (#{e.class})")
303
+ retry
304
+ rescue
305
+ end
306
+ end
307
+
308
+ raise
309
+ end
310
+ end
311
+
312
+ # DELETE /domain/key
313
+ def delete(env)
314
+ case env["PATH_INFO"]
315
+ when %r{\A/([^/]+)/(.+)\z} # /$DOMAIN/$KEY
316
+ delete_key(env, $1, $2)
317
+ else
318
+ r(404, "")
319
+ end
320
+ end
321
+
322
+ # DELETE /domain/key
323
+ def delete_key(env, domain, key)
324
+ pool_use(domain) { |mg| mg.delete(key) }
325
+ r(204, "")
326
+ rescue MogileFS::Backend::UnregDomainError,
327
+ MogileFS::Backend::DomainNotFoundError
328
+ r(406, "Invalid domain: #{domain}")
329
+ rescue MogileFS::Backend::UnknownKeyError
330
+ r(404, "")
331
+ end
332
+
333
+ def query(env)
334
+ Rack::Utils.parse_query(env["QUERY_STRING"])
335
+ end
336
+ end
@@ -0,0 +1,48 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2008-2012, Eric Wong <normalperson@yhbt.net>
3
+ # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
4
+ require "thread"
5
+ require "omgf"
6
+
7
+ module OMGF::Pool
8
+ # TODO: backport these improvements to MogileFS::Pool (but break compat?)
9
+ def pool_init(mg_opts)
10
+ @mg_opts = mg_opts
11
+ @pool = []
12
+ @lock = Mutex.new
13
+ end
14
+
15
+ def pool_use(domain)
16
+ # Array#pop is faster than Queue#pop since Queue#pop shifts off
17
+ # the array internally, and taking the last element off the end is faster.
18
+ mg = @lock.synchronize { @pool.pop || MogileFS::MogileFS.new(@mg_opts) }
19
+ mg.domain = domain
20
+ rv = yield mg
21
+ @lock.synchronize do
22
+ if @pool.size < 3
23
+ @pool << mg
24
+ mg = nil # prevent shutdown
25
+ end
26
+ end
27
+ rv
28
+ ensure
29
+ # shutdown if we didn't return to the pool
30
+ if mg
31
+ mg.backend.shutdown rescue nil
32
+ end
33
+ end
34
+
35
+ def mg_list_keys(domain, prefix, after, limit, &block) # :nodoc:
36
+ pool_use(domain) { |mg| mg.list_keys(prefix, after, limit, &block) }
37
+ end
38
+
39
+ def mg_get_uris(domain, key, opts) # :nodoc:
40
+ pool_use(domain) { |mg| mg.get_uris(key, opts) }
41
+ end
42
+
43
+ def mg_size_and_paths(domain, key, opts) # :nodoc:
44
+ pool_use(domain) do |mg|
45
+ [ mg.size(key), mg.get_paths(key, opts) ]
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: binary -*-
2
+ ENV["VERSION"] or abort "VERSION= must be specified"
3
+ manifest = File.readlines('.manifest').map! { |x| x.chomp! }
4
+ require 'wrongdoc'
5
+ extend Wrongdoc::Gemspec
6
+ name, summary, title = readme_metadata
7
+
8
+ Gem::Specification.new do |s|
9
+ s.name = %q{omgf}
10
+ s.version = ENV["VERSION"].dup
11
+
12
+ s.authors = ["OMGF hackers"]
13
+ s.date = Time.now.utc.strftime('%Y-%m-%d')
14
+ s.description = readme_description
15
+ s.email = %q{omgf@librelist.org}
16
+ s.extra_rdoc_files = extra_rdoc_files(manifest)
17
+ s.files = manifest
18
+ s.homepage = Wrongdoc.config[:rdoc_url]
19
+ s.summary = summary
20
+ s.rdoc_options = rdoc_options
21
+ s.test_files = Dir['test/test_*.rb']
22
+ s.add_dependency('rack', ['~> 1.3'])
23
+ s.add_dependency('mogilefs-client', ['~> 3.1'])
24
+
25
+ s.licenses = %w(AGPLv3+)
26
+ end
data/pkg.mk ADDED
@@ -0,0 +1,175 @@
1
+ RUBY = ruby
2
+ RAKE = rake
3
+ RSYNC = rsync
4
+ WRONGDOC = wrongdoc
5
+
6
+ GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE
7
+ @./GIT-VERSION-GEN
8
+ -include GIT-VERSION-FILE
9
+ -include local.mk
10
+ DLEXT := $(shell $(RUBY) -rrbconfig -e 'puts RbConfig::CONFIG["DLEXT"]')
11
+ RUBY_VERSION := $(shell $(RUBY) -e 'puts RUBY_VERSION')
12
+ RUBY_ENGINE := $(shell $(RUBY) -e 'puts((RUBY_ENGINE rescue "ruby"))')
13
+ lib := lib
14
+
15
+ ifeq ($(shell test -f script/isolate_for_tests && echo t),t)
16
+ isolate_libs := tmp/isolate/$(RUBY_ENGINE)-$(RUBY_VERSION)/isolate.mk
17
+ $(isolate_libs): script/isolate_for_tests
18
+ @$(RUBY) script/isolate_for_tests
19
+ -include $(isolate_libs)
20
+ lib := $(lib):$(ISOLATE_LIBS)
21
+ endif
22
+
23
+ ext := $(firstword $(wildcard ext/*))
24
+ ifneq ($(ext),)
25
+ ext_pfx := tmp/ext/$(RUBY_ENGINE)-$(RUBY_VERSION)
26
+ ext_h := $(wildcard $(ext)/*/*.h $(ext)/*.h)
27
+ ext_src := $(wildcard $(ext)/*.c $(ext_h))
28
+ ext_pfx_src := $(addprefix $(ext_pfx)/,$(ext_src))
29
+ ext_d := $(ext_pfx)/$(ext)/.d
30
+ $(ext)/extconf.rb: $(wildcard $(ext)/*.h)
31
+ @>> $@
32
+ $(ext_d):
33
+ @mkdir -p $(@D)
34
+ @> $@
35
+ $(ext_pfx)/$(ext)/%: $(ext)/% $(ext_d)
36
+ install -m 644 $< $@
37
+ $(ext_pfx)/$(ext)/Makefile: $(ext)/extconf.rb $(ext_d) $(ext_h)
38
+ $(RM) -f $(@D)/*.o
39
+ cd $(@D) && $(RUBY) $(CURDIR)/$(ext)/extconf.rb
40
+ ext_sfx := _ext.$(DLEXT)
41
+ ext_dl := $(ext_pfx)/$(ext)/$(notdir $(ext)_ext.$(DLEXT))
42
+ $(ext_dl): $(ext_src) $(ext_pfx_src) $(ext_pfx)/$(ext)/Makefile
43
+ @echo $^ == $@
44
+ $(MAKE) -C $(@D)
45
+ lib := $(lib):$(ext_pfx)/$(ext)
46
+ build: $(ext_dl)
47
+ else
48
+ build:
49
+ endif
50
+
51
+ pkg_extra += GIT-VERSION-FILE NEWS ChangeLog LATEST
52
+ ChangeLog: GIT-VERSION-FILE .wrongdoc.yml
53
+ $(WRONGDOC) prepare
54
+ NEWS LATEST: ChangeLog
55
+
56
+ manifest:
57
+ $(RM) .manifest
58
+ $(MAKE) .manifest
59
+
60
+ .manifest: $(pkg_extra)
61
+ (git ls-files && for i in $@ $(pkg_extra); do echo $$i; done) | \
62
+ LC_ALL=C sort > $@+
63
+ cmp $@+ $@ || mv $@+ $@
64
+ $(RM) $@+
65
+
66
+ doc:: .document .wrongdoc.yml $(pkg_extra)
67
+ -find lib -type f -name '*.rbc' -exec rm -f '{}' ';'
68
+ -find ext -type f -name '*.rbc' -exec rm -f '{}' ';'
69
+ $(RM) -r doc
70
+ $(WRONGDOC) all
71
+ install -m644 COPYING doc/COPYING
72
+ install -m644 $(shell LC_ALL=C grep '^[A-Z]' .document) doc/
73
+
74
+ ifneq ($(VERSION),)
75
+ pkggem := pkg/$(rfpackage)-$(VERSION).gem
76
+ pkgtgz := pkg/$(rfpackage)-$(VERSION).tgz
77
+ release_notes := release_notes-$(VERSION)
78
+ release_changes := release_changes-$(VERSION)
79
+
80
+ release-notes: $(release_notes)
81
+ release-changes: $(release_changes)
82
+ $(release_changes):
83
+ $(WRONGDOC) release_changes > $@+
84
+ $(VISUAL) $@+ && test -s $@+ && mv $@+ $@
85
+ $(release_notes):
86
+ $(WRONGDOC) release_notes > $@+
87
+ $(VISUAL) $@+ && test -s $@+ && mv $@+ $@
88
+
89
+ # ensures we're actually on the tagged $(VERSION), only used for release
90
+ verify:
91
+ test x"$(shell umask)" = x0022
92
+ git rev-parse --verify refs/tags/v$(VERSION)^{}
93
+ git diff-index --quiet HEAD^0
94
+ test $$(git rev-parse --verify HEAD^0) = \
95
+ $$(git rev-parse --verify refs/tags/v$(VERSION)^{})
96
+
97
+ fix-perms:
98
+ -git ls-tree -r HEAD | awk '/^100644 / {print $$NF}' | xargs chmod 644
99
+ -git ls-tree -r HEAD | awk '/^100755 / {print $$NF}' | xargs chmod 755
100
+
101
+ gem: $(pkggem)
102
+
103
+ install-gem: $(pkggem)
104
+ gem install $(CURDIR)/$<
105
+
106
+ $(pkggem): manifest fix-perms
107
+ gem build $(rfpackage).gemspec
108
+ mkdir -p pkg
109
+ mv $(@F) $@
110
+
111
+ $(pkgtgz): distdir = $(basename $@)
112
+ $(pkgtgz): HEAD = v$(VERSION)
113
+ $(pkgtgz): manifest fix-perms
114
+ @test -n "$(distdir)"
115
+ $(RM) -r $(distdir)
116
+ mkdir -p $(distdir)
117
+ tar cf - $$(cat .manifest) | (cd $(distdir) && tar xf -)
118
+ cd pkg && tar cf - $(basename $(@F)) | gzip -9 > $(@F)+
119
+ mv $@+ $@
120
+
121
+ package: $(pkgtgz) $(pkggem)
122
+
123
+ test-release:: verify package $(release_notes) $(release_changes)
124
+ # make tgz release on RubyForge
125
+ @echo rubyforge add_release -f \
126
+ -n $(release_notes) -a $(release_changes) \
127
+ $(rfproject) $(rfpackage) $(VERSION) $(pkgtgz)
128
+ @echo gem push $(pkggem)
129
+ @echo rubyforge add_file \
130
+ $(rfproject) $(rfpackage) $(VERSION) $(pkggem)
131
+ release:: verify package $(release_notes) $(release_changes)
132
+ # make tgz release on RubyForge
133
+ rubyforge add_release -f -n $(release_notes) -a $(release_changes) \
134
+ $(rfproject) $(rfpackage) $(VERSION) $(pkgtgz)
135
+ # push gem to RubyGems.org
136
+ gem push $(pkggem)
137
+ # in case of gem downloads from RubyForge releases page
138
+ rubyforge add_file \
139
+ $(rfproject) $(rfpackage) $(VERSION) $(pkggem)
140
+ else
141
+ gem install-gem: GIT-VERSION-FILE
142
+ $(MAKE) $@ VERSION=$(GIT_VERSION)
143
+ endif
144
+
145
+ all:: test
146
+ test_units := $(wildcard test/test_*.rb)
147
+ test: test-unit
148
+ test-unit: $(test_units)
149
+ $(test_units): build
150
+ $(RUBY) -I $(lib) $@ $(RUBY_TEST_OPTS)
151
+
152
+ # this requires GNU coreutils variants
153
+ ifneq ($(RSYNC_DEST),)
154
+ publish_doc:
155
+ -git set-file-times
156
+ $(MAKE) doc
157
+ find doc/images -type f | \
158
+ TZ=UTC xargs touch -d '1970-01-01 00:00:06' doc/rdoc.css
159
+ $(MAKE) doc_gz
160
+ $(RSYNC) -av doc/ $(RSYNC_DEST)/
161
+ git ls-files | xargs touch
162
+ endif
163
+
164
+ # Create gzip variants of the same timestamp as the original so nginx
165
+ # "gzip_static on" can serve the gzipped versions directly.
166
+ doc_gz: docs = $(shell find doc -type f ! -regex '^.*\.\(gif\|jpg\|png\|gz\)$$')
167
+ doc_gz:
168
+ for i in $(docs); do \
169
+ gzip --rsyncable -9 < $$i > $$i.gz; touch -r $$i $$i.gz; done
170
+ check-warnings:
171
+ @(for i in $$(git ls-files '*.rb'| grep -v '^setup\.rb$$'); \
172
+ do $(RUBY) -d -W2 -c $$i; done) | grep -v '^Syntax OK$$' || :
173
+
174
+ .PHONY: all .FORCE-GIT-VERSION-FILE doc test $(test_units) manifest
175
+ .PHONY: check-warnings