sinatra-s3 0.98

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.
Files changed (75) hide show
  1. data/README +23 -0
  2. data/Rakefile +51 -0
  3. data/bin/sinatra-s3 +30 -0
  4. data/db/migrate/001_create_bits.rb +28 -0
  5. data/db/migrate/002_create_users.rb +24 -0
  6. data/db/migrate/003_create_bits_users.rb +16 -0
  7. data/db/migrate/004_create_torrents.rb +22 -0
  8. data/db/migrate/005_create_torrent_peers.rb +26 -0
  9. data/examples/README +9 -0
  10. data/examples/wiki.rb +199 -0
  11. data/examples/wiki.ru +5 -0
  12. data/examples/wikicloth/MIT-LICENSE +20 -0
  13. data/examples/wikicloth/README +81 -0
  14. data/examples/wikicloth/Rakefile +23 -0
  15. data/examples/wikicloth/init.rb +1 -0
  16. data/examples/wikicloth/install.rb +0 -0
  17. data/examples/wikicloth/lib/core_ext.rb +43 -0
  18. data/examples/wikicloth/lib/wiki_buffer/html_element.rb +237 -0
  19. data/examples/wikicloth/lib/wiki_buffer/link.rb +70 -0
  20. data/examples/wikicloth/lib/wiki_buffer/table.rb +159 -0
  21. data/examples/wikicloth/lib/wiki_buffer/var.rb +77 -0
  22. data/examples/wikicloth/lib/wiki_buffer.rb +279 -0
  23. data/examples/wikicloth/lib/wiki_cloth.rb +61 -0
  24. data/examples/wikicloth/lib/wiki_link_handler.rb +138 -0
  25. data/examples/wikicloth/lib/wikicloth.rb +5 -0
  26. data/examples/wikicloth/run_tests.rb +48 -0
  27. data/examples/wikicloth/sample_documents/air_force_one.wiki +170 -0
  28. data/examples/wikicloth/sample_documents/cheatsheet.wiki +205 -0
  29. data/examples/wikicloth/sample_documents/default.css +34 -0
  30. data/examples/wikicloth/sample_documents/elements.wiki +7 -0
  31. data/examples/wikicloth/sample_documents/george_washington.wiki +526 -0
  32. data/examples/wikicloth/sample_documents/images.wiki +15 -0
  33. data/examples/wikicloth/sample_documents/lists.wiki +421 -0
  34. data/examples/wikicloth/sample_documents/pipe_trick.wiki +68 -0
  35. data/examples/wikicloth/sample_documents/random.wiki +55 -0
  36. data/examples/wikicloth/sample_documents/tv.wiki +312 -0
  37. data/examples/wikicloth/sample_documents/wiki.png +0 -0
  38. data/examples/wikicloth/sample_documents/wiki_tables.wiki +410 -0
  39. data/examples/wikicloth/tasks/wikicloth_tasks.rake +0 -0
  40. data/examples/wikicloth/test/test_helper.rb +3 -0
  41. data/examples/wikicloth/test/wiki_cloth_test.rb +8 -0
  42. data/examples/wikicloth/uninstall.rb +0 -0
  43. data/examples/wikicloth/wikicloth-0.1.3.gem +0 -0
  44. data/examples/wikicloth/wikicloth.gemspec +69 -0
  45. data/lib/sinatra-s3/admin.rb +626 -0
  46. data/lib/sinatra-s3/base.rb +526 -0
  47. data/lib/sinatra-s3/errors.rb +51 -0
  48. data/lib/sinatra-s3/ext.rb +20 -0
  49. data/lib/sinatra-s3/helpers/acp.rb +100 -0
  50. data/lib/sinatra-s3/helpers/admin.rb +41 -0
  51. data/lib/sinatra-s3/helpers/tracker.rb +42 -0
  52. data/lib/sinatra-s3/helpers/versioning.rb +27 -0
  53. data/lib/sinatra-s3/helpers.rb +79 -0
  54. data/lib/sinatra-s3/models/bit.rb +180 -0
  55. data/lib/sinatra-s3/models/bucket.rb +81 -0
  56. data/lib/sinatra-s3/models/file_info.rb +3 -0
  57. data/lib/sinatra-s3/models/git_bucket.rb +3 -0
  58. data/lib/sinatra-s3/models/slot.rb +47 -0
  59. data/lib/sinatra-s3/models/torrent.rb +6 -0
  60. data/lib/sinatra-s3/models/torrent_peer.rb +5 -0
  61. data/lib/sinatra-s3/models/user.rb +35 -0
  62. data/lib/sinatra-s3/s3.rb +57 -0
  63. data/lib/sinatra-s3/tasks.rb +62 -0
  64. data/lib/sinatra-s3/tracker.rb +134 -0
  65. data/lib/sinatra-s3.rb +1 -0
  66. data/public/css/control.css +225 -0
  67. data/public/css/wiki.css +47 -0
  68. data/public/images/external-link.gif +0 -0
  69. data/public/js/prototype.js +2539 -0
  70. data/public/js/upload_status.js +117 -0
  71. data/public/test.html +8 -0
  72. data/s3.yml.example +17 -0
  73. data/test/s3api_test.rb +121 -0
  74. data/test/test_helper.rb +25 -0
  75. metadata +156 -0
@@ -0,0 +1,526 @@
1
+ module S3
2
+
3
+ def self.config
4
+ @config ||= YAML.load_file("s3.yml")[S3::Application.environment] rescue { :db => { :adapter => 'sqlite3', :database => "db/s3.db" } }
5
+ end
6
+
7
+ class Application < Sinatra::Base
8
+
9
+ enable :static
10
+ disable :raise_errors, :show_exceptions
11
+ set :environment, :production
12
+ set :public, PUBLIC_PATH
13
+
14
+ helpers do
15
+ include S3::Helpers
16
+ include S3::TrackerHelper
17
+ end
18
+
19
+ configure do
20
+ ActiveRecord::Base.establish_connection(S3.config[:db])
21
+ end
22
+
23
+ before do
24
+ run_callback_for :when => 'before'
25
+
26
+ @meta, @amz = {}, {}
27
+ @env.each do |k,v|
28
+ k = k.downcase.gsub('_', '-')
29
+ @amz[$1] = v.strip if k =~ /^http-x-amz-([-\w]+)$/
30
+ @meta[$1] = v if k =~ /^http-x-amz-meta-([-\w]+)$/
31
+ end
32
+
33
+ auth, key_s, secret_s = *env['HTTP_AUTHORIZATION'].to_s.match(/^AWS (\w+):(.+)$/)
34
+ date_s = env['HTTP_X_AMZ_DATE'] || env['HTTP_DATE']
35
+ if request.params.has_key?('Signature') and Time.at(request['Expires'].to_i) >= Time.now
36
+ key_s, secret_s, date_s = request['AWSAccessKeyId'], request['Signature'], request['Expires']
37
+ end
38
+ uri = env['PATH_INFO']
39
+ uri += "?" + env['QUERY_STRING'] if RESOURCE_TYPES.include?(env['QUERY_STRING'])
40
+ canonical = [env['REQUEST_METHOD'], env['HTTP_CONTENT_MD5'], env['CONTENT_TYPE'],
41
+ date_s, uri]
42
+ @amz.sort.each do |k, v|
43
+ canonical[-1,0] = "x-amz-#{k}:#{v}"
44
+ end
45
+
46
+ @user = User.find_by_key key_s
47
+ if (@user and secret_s != hmac_sha1(@user.secret, canonical.map{|v|v.to_s.strip} * "\n")) || (@user and @user.deleted == 1)
48
+ raise BadAuthentication
49
+ end
50
+
51
+ @request_id = Time.now.to_i
52
+ headers 'x-amz-request-id' => @request_id.to_s
53
+ end
54
+
55
+ def call(env)
56
+ begin
57
+ return if env['PATH_INFO'] =~ /^\/control/
58
+ super(env)
59
+ ensure
60
+ ActiveRecord::Base.connection_pool.release_connection
61
+ end
62
+ end
63
+
64
+ get '/' do
65
+ only_authorized
66
+ buckets = Bucket.user_buckets(@user.id)
67
+
68
+ xml do |x|
69
+ x.ListAllMyBucketsResult :xmlns => "http://s3.amazonaws.com/doc/2006-03-01/" do
70
+ x.Owner do
71
+ x.ID @user.key
72
+ x.DisplayName @user.login
73
+ end
74
+ x.Buckets do
75
+ buckets.each do |b|
76
+ x.Bucket do
77
+ x.Name b.name
78
+ x.CreationDate b.created_at.getgm.iso8601
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+
87
+ # get bucket
88
+ get %r{^/([^\/]+)/?$} do
89
+ bucket = Bucket.find_root(params[:captures].first)
90
+ acl_response_for(bucket) and return if params.has_key?('acl')
91
+ versioning_response_for(bucket) and return if params.has_key?('versioning')
92
+ only_can_read bucket
93
+
94
+ params['prefix'] ||= ''
95
+ params['marker'] ||= ''
96
+
97
+ query = bucket.items(params['marker'],params['prefix'])
98
+ slot_count = query.count
99
+ contents = query.find(:all, :include => :owner,
100
+ :limit => params['max-keys'].blank? ? 1000 : params['max-keys'])
101
+
102
+ if params['delimiter']
103
+ # Build a hash of { :prefix => content_key }. The prefix will not include the supplied params['prefix'].
104
+ prefixes = contents.inject({}) do |hash, c|
105
+ prefix = get_prefix(c).to_sym
106
+ hash[prefix] = [] unless hash[prefix]
107
+ hash[prefix] << c.name
108
+ hash
109
+ end
110
+
111
+ # The common prefixes are those with more than one element
112
+ common_prefixes = prefixes.inject([]) do |array, prefix|
113
+ array << prefix[0].to_s if prefix[1].size > 1
114
+ array
115
+ end
116
+
117
+ # The contents are everything that doesn't have a common prefix
118
+ contents = contents.reject do |c|
119
+ common_prefixes.include? get_prefix(c)
120
+ end
121
+ end
122
+
123
+ xml do |x|
124
+ x.ListBucketResult :xmlns => "http://s3.amazonaws.com/doc/2006-03-01/" do
125
+ x.Name bucket.name
126
+ x.Prefix params['prefix']
127
+ x.Marker params['marker']
128
+ x.Delimiter params['delimiter'] if params['delimiter']
129
+ x.MaxKeys params['max-keys'].blank? ? 1000 : params['max-keys']
130
+ x.IsTruncated slot_count > contents.length
131
+ contents.each do |c|
132
+ x.Contents do
133
+ x.Key c.name
134
+ x.LastModified c.updated_at.getgm.iso8601
135
+ x.ETag c.etag
136
+ x.Size c.obj.size
137
+ x.StorageClass "STANDARD"
138
+ x.Owner do
139
+ x.ID c.owner.key
140
+ x.DisplayName c.owner.login
141
+ end
142
+ end
143
+ end
144
+ unless common_prefixes.nil?
145
+ common_prefixes.each do |p|
146
+ x.CommonPrefixes do
147
+ x.Prefix p
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ # create bucket
156
+ put %r{^/([^\/]+)/?$} do
157
+ begin
158
+ only_authorized
159
+ bucket = Bucket.find_root(params[:captures].first)
160
+ only_owner_of bucket
161
+ if params.has_key?('acl')
162
+ bucket.grant(requested_acl(bucket))
163
+ elsif params.has_key?('versioning')
164
+ manage_versioning(bucket)
165
+ else
166
+ raise BucketAlreadyExists
167
+ end
168
+ headers 'Location' => env['PATH_INFO'], 'Content-Length' => 0.to_s
169
+ body ""
170
+ rescue NoSuchBucket
171
+ Bucket.create(:name => params[:captures].first, :owner_id => @user.id).grant(requested_acl)
172
+ headers 'Location' => env['PATH_INFO'], 'Content-Length' => 0.to_s
173
+ body ""
174
+ end
175
+ end
176
+
177
+ # delete bucket
178
+ delete %r{^/([^\/]+)/?$} do
179
+ bucket = Bucket.find_root(params[:captures].first)
180
+ only_owner_of bucket
181
+
182
+ raise BucketNotEmpty if Slot.count(:conditions => ['deleted = 0 AND parent_id = ?', bucket.id]) > 0
183
+
184
+ bucket.remove_from_filesystem
185
+ bucket.destroy
186
+ status 204
187
+ body ""
188
+ end
189
+
190
+ # get slot (head)
191
+ head %r{^/(.+?)/(.+)$} do
192
+ slot_head
193
+ body ""
194
+ end
195
+
196
+ def slot_head
197
+ bucket = Bucket.find_root(params[:captures].first)
198
+
199
+ h = {}
200
+ if params.has_key?('version-id')
201
+ @revision = bucket.git_repository.gcommit(params['version-id'])
202
+ h.merge!({ 'x-amz-version-id' => @revision.sha })
203
+ @slot = Slot.find_by_version(@revision.sha)
204
+ @revision_file = @revision.gtree.blobs[File.basename(@slot.fullpath)].contents { |f| f.read }
205
+ else
206
+ @slot = bucket.find_slot(params[:captures].last)
207
+ git_object = @slot.git_object
208
+ h.merge!({ 'x-amz-version-id' => git_object.objectish }) if git_object
209
+ end
210
+
211
+ if params.has_key? 'acl'
212
+ only_can_read_acp @slot
213
+ else
214
+ only_can_read @slot
215
+ end
216
+
217
+ etag = @slot.etag
218
+ since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']) rescue nil
219
+ raise NotModified if since and @slot.updated_at <= since
220
+ since = Time.httpdate(env['HTTP_IF_UNMODIFIED_SINCE']) rescue nil
221
+ raise PreconditionFailed if since and @slot.updated_at > since
222
+ raise PreconditionFailed if env['HTTP_IF_MATCH'] and etag != env['HTTP_IF_MATCH']
223
+ raise NotModified if env['HTTP_IF_NONE_MATCH'] and etag == env['HTTP_IF_NONE_MATCH']
224
+
225
+ @slot.meta.each { |k, v|
226
+ h.merge!({ "x-amz-meta-#{k}" => v })
227
+ }
228
+
229
+ if @slot.obj.is_a? FileInfo
230
+ h.merge!({ 'Content-Disposition' => (@slot.obj.disposition.nil? ? "inline" : @slot.obj.disposition), 'Content-Length' => (@revision_file.nil? ?
231
+ @slot.obj.size : @revision_file.length).to_s, 'Content-Type' => @slot.obj.mime_type })
232
+ end
233
+ h['Content-Type'] ||= 'binary/octet-stream'
234
+ h.merge!('ETag' => etag, 'Last-Modified' => @slot.updated_at.httpdate) if @revision_file.nil?
235
+ headers h
236
+ end
237
+
238
+ # get slot
239
+ get %r{^/(.+?)/(.+)$} do
240
+ slot_head
241
+ acl_response_for(@slot) and return if params.has_key?('acl')
242
+
243
+ if params.has_key?('torrent')
244
+ torrent @slot
245
+ elsif @slot.obj.kind_of?(FileInfo) && env['HTTP_RANGE'] =~ /^bytes=(\d+)?-(\d+)?$/ # yay, parse basic ranges
246
+ range_start = $1
247
+ range_end = $2
248
+ raise NotImplemented unless range_start || range_end # Need at least one or the other.
249
+ file_path = File.join(STORAGE_PATH, @slot.obj.path)
250
+ file_size = File.size(file_path)
251
+ f = File.open(file_path)
252
+ if range_start # "Bytes N through ?" mode
253
+ range_end = (file_size - 1) if range_end.nil?
254
+ content_length = (range_end.to_i - range_start.to_i + 1)
255
+ headers['Content-Range'] = "bytes #{range_start.to_i}-#{range_end.to_i}/#{file_size}"
256
+ else # "Last N bytes of file" mode.
257
+ range_start = file_size - range_end.to_i
258
+ content_length = range_end.to_i
259
+ headers['Content-Range'] = "bytes #{range_start.to_i}-#{file_size - 1}/#{file_size}"
260
+ end
261
+ f.seek(range_start.to_i)
262
+ status 206
263
+ headers['Content-Length'] = ([content_length,0].max).to_s
264
+ body f
265
+ elsif env['HTTP_RANGE'] # ugh, parse ranges
266
+ raise NotImplemented
267
+ else
268
+ case @slot.obj
269
+ when FileInfo
270
+ body params.has_key?('version-id') ? @revision_file : open(File.join(STORAGE_PATH, @slot.obj.path))
271
+ run_callback_for :mime_type => @slot.obj.mime_type
272
+ else
273
+ body @slot.obj
274
+ end
275
+ end
276
+ end
277
+
278
+ # create slot
279
+ post %r{^/(.+?)/(.+)$} do
280
+ params.each do |k,v|
281
+ case
282
+ when k =~ /^x-amz-meta-(.*)$/
283
+ @meta[$1] = v
284
+ when k =~ /content-type/i
285
+ env['CONTENT_TYPE'] = v
286
+ when k =~ /content-disposition/i
287
+ env['CONTENT_DISPOSITION'] = v
288
+ end
289
+ end
290
+ env['rack.input'] = params[:file].instance_of?(File) ? params[:file] : StringIO.new(params[:file])
291
+ env['CONTENT_LENGTH'] = env['rack.input'].length
292
+ create_slot
293
+ end
294
+
295
+ # create slot
296
+ put %r{^/(.+?)/(.+)$} do
297
+ create_slot
298
+ end
299
+
300
+ def create_slot
301
+ bucket = Bucket.find_root(params[:captures].first)
302
+ begin
303
+ slot = bucket.find_slot(params[:captures].last)
304
+ only_can_write slot unless slot.nil?
305
+ rescue NoSuchKey
306
+ only_can_write bucket
307
+ end
308
+
309
+ raise MissingContentLength unless env['CONTENT_LENGTH']
310
+
311
+ if params.has_key?('acl')
312
+ slot = bucket.find_slot(oid)
313
+ slot.grant(requested_acl(slot))
314
+ headers 'ETag' => slot.etag, 'Content-Length' => 0.to_s
315
+ body ""
316
+ elsif env['HTTP_X_AMZ_COPY_SOURCE'].to_s =~ /\/(.+?)\/(.+)/
317
+ source_bucket_name = $1
318
+ source_oid = $2
319
+
320
+ source_slot = Bucket.find_root(source_bucket_name).find_slot(source_oid)
321
+ @meta = source_slot.meta unless !env['HTTP_X_AMZ_METADATA_DIRECTIVE'].nil? && env['HTTP_X_AMZ_METADATA_DIRECTIVE'].upcase == "REPLACE"
322
+ only_can_read source_slot
323
+
324
+ unless env['HTTP_X_AMZ_COPY_SOURCE_IF_MATCH'].blank?
325
+ raise PreconditionFailed if source_slot.obj.etag != env['HTTP_X_AMZ_COPY_SOURCE_IF_MATCH']
326
+ end
327
+ unless env['HTTP_X_AMZ_COPY_SOURCE_IF_NONE_MATCH'].blank?
328
+ raise PreconditionFailed if source_slot.obj.etag == env['HTTP_X_AMZ_COPY_SOURCE_IF_NONE_MATCH']
329
+ end
330
+ unless env['HTTP_X_AMZ_COPY_SOURCE_IF_UNMODIFIED_SINCE'].blank?
331
+ raise PreconditionFailed if Time.httpdate(env['HTTP_X_AMZ_COPY_SOURCE_IF_UNMODIFIED_SINCE']) > source_slot.updated_at
332
+ end
333
+ unless env['HTTP_X_AMZ_COPY_SOURCE_IF_MODIFIED_SINCE'].blank?
334
+ raise PreconditionFailed if Time.httpdate(env['HTTP_X_AMZ_COPY_SOURCE_IF_MODIFIED_SINCE']) < source_slot.updated_at
335
+ end
336
+
337
+ temp_path = File.join(STORAGE_PATH, source_slot.obj.path)
338
+ fileinfo = source_slot.obj
339
+ fileinfo.path = File.join(params[:captures].first, rand(10000).to_s(36) + '_' + File.basename(temp_path))
340
+ fileinfo.path.succ! while File.exists?(File.join(STORAGE_PATH, fileinfo.path))
341
+ file_path = File.join(STORAGE_PATH,fileinfo.path)
342
+ else
343
+ temp_path = env['rack.input'][:path] rescue nil
344
+ readlen = 0
345
+ md5 = MD5.new
346
+
347
+ Tempfile.open(File.basename(params[:captures].last)) do |tmpf|
348
+ temp_path ||= tmpf.path
349
+ tmpf.binmode
350
+ while part = env['rack.input'].read(BUFSIZE)
351
+ readlen += part.size
352
+ md5 << part
353
+ tmpf << part unless env['rack.input'].is_a?(Tempfile)
354
+ end
355
+ end
356
+
357
+ fileinfo = FileInfo.new
358
+ fileinfo.mime_type = env['CONTENT_TYPE'] || "binary/octet-stream"
359
+ fileinfo.disposition = env['CONTENT_DISPOSITION']
360
+ fileinfo.size = readlen
361
+ fileinfo.md5 = Base64.encode64(md5.digest).strip
362
+ fileinfo.etag = '"' + md5.hexdigest + '"'
363
+
364
+ raise IncompleteBody if env['CONTENT_LENGTH'].to_i != readlen
365
+ if env['HTTP_CONTENT_MD5']
366
+ b64cs = /[0-9a-zA-Z+\/]/
367
+ re = /
368
+ ^
369
+ (?:#{b64cs}{4})* # any four legal chars
370
+ (?:#{b64cs}{2} # right-padded by up to two =s
371
+ (?:#{b64cs}|=){2})?
372
+ $
373
+ /ox
374
+
375
+ raise InvalidDigest unless env['HTTP_CONTENT_MD5'] =~ re
376
+ raise BadDigest unless fileinfo.md5 == env['HTTP_CONTENT_MD5']
377
+ end
378
+ end
379
+
380
+ mdata = {}
381
+
382
+ slot = nil
383
+ meta = @meta.nil? || @meta.empty? ? {} : {}.merge(@meta)
384
+ owner_id = @user ? @user.id : bucket.owner_id
385
+
386
+ begin
387
+ slot = bucket.find_slot(params[:captures].last)
388
+ if slot.versioning_enabled?
389
+ nslot = slot.clone()
390
+ slot.update_attributes(:deleted => true)
391
+ slot = nslot
392
+ end
393
+ if source_slot.nil?
394
+ fileinfo.path = slot.obj.path
395
+ file_path = File.join(STORAGE_PATH,fileinfo.path)
396
+ FileUtils.mv(temp_path, file_path,{ :force => true })
397
+ else
398
+ FileUtils.cp(temp_path, file_path)
399
+ end
400
+ slot.update_attributes(:owner_id => owner_id, :meta => meta, :obj => fileinfo, :size => fileinfo.size)
401
+ rescue NoSuchKey
402
+ if source_slot.nil?
403
+ fileinfo.path = File.join(params[:captures].first, rand(10000).to_s(36) + '_' + File.basename(temp_path))
404
+ fileinfo.path.succ! while File.exists?(File.join(STORAGE_PATH, fileinfo.path))
405
+ file_path = File.join(STORAGE_PATH,fileinfo.path)
406
+ FileUtils.mkdir_p(File.dirname(file_path))
407
+ FileUtils.mv(temp_path, file_path)
408
+ else
409
+ FileUtils.cp(temp_path, file_path)
410
+ end
411
+ slot = Slot.create(:name => params[:captures].last, :owner_id => owner_id, :meta => meta, :obj => fileinfo, :size => fileinfo.size)
412
+ bucket.add_child(slot)
413
+ end
414
+ slot.grant(requested_acl(slot))
415
+
416
+ h = { 'Content-Length' => 0.to_s, 'ETag' => slot.etag }
417
+ if slot.versioning_enabled?
418
+ begin
419
+ slot.git_repository.add(File.basename(fileinfo.path))
420
+ tmp = slot.git_repository.commit("Added #{slot.name} to the Git repository.")
421
+ slot.git_update
422
+ slot.update_attributes(:version => slot.git_object.objectish)
423
+ h.merge!({ 'x-amz-version-id' => slot.git_object.objectish })
424
+ rescue Git::GitExecuteError => error_message
425
+ puts "[#{Time.now}] GIT: #{error_message}"
426
+ end
427
+ end
428
+
429
+ if env['HTTP_X_AMZ_COPY_SOURCE'].blank?
430
+ redirect_url = (params[:success_action_redirect] || params[:redirect])
431
+ redirect redirect_url unless redirect_url.blank?
432
+ status params[:success_action_status].to_i if params[:success_action_status]
433
+ headers h
434
+ body ""
435
+ else
436
+ h['Content-Length'] = nil
437
+ headers h
438
+ xml do |x|
439
+ x.CopyObjectResult do
440
+ x.LastModified slot.updated_at.httpdate
441
+ x.Etag slot.etag
442
+ end
443
+ end
444
+ end
445
+ end
446
+
447
+ # delete slot
448
+ delete %r{^/(.+?)/(.+)$} do
449
+ bucket = Bucket.find_root(params[:captures].first)
450
+ only_can_write bucket
451
+
452
+ begin
453
+ @slot = bucket.find_slot(params[:captures].last)
454
+ if @slot.versioning_enabled?
455
+ begin
456
+ @slot.git_repository.remove(File.basename(@slot.obj.path))
457
+ @slot.git_repository.commit("Removed #{@slot.name} from the Git repository.")
458
+ @slot.git_update
459
+ rescue Git::GitExecuteError => error_message
460
+ puts "[#{Time.now}] GIT: #{error_message}"
461
+ end
462
+ end
463
+
464
+ @slot.remove_from_filesystem
465
+ @slot.destroy
466
+ status 204
467
+ body ""
468
+ rescue NoSuchKey
469
+ status 204
470
+ body ""
471
+ end
472
+ end
473
+
474
+ error do
475
+ error = Builder::XmlMarkup.new
476
+ error.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
477
+
478
+ error.Error do
479
+ error.Code request.env['sinatra.error'].code
480
+ error.Message request.env['sinatra.error'].message
481
+ error.Resource env['PATH_INFO']
482
+ error.RequestId @request_id
483
+ end
484
+
485
+ status request.env['sinatra.error'].status.nil? ? 500 : request.env['sinatra.error'].status
486
+ content_type 'application/xml'
487
+ body error.target!
488
+ run_callback_for :error => request.env['sinatra.error'].code
489
+ end
490
+
491
+ def self.callback(args = {}, &block)
492
+ @@callbacks ||= {}
493
+ if args[:mime_type]
494
+ @@callbacks[:mime_type] ||= {}
495
+ @@callbacks[:mime_type][args[:mime_type]] = block
496
+ elsif args[:error]
497
+ @@callbacks[:error] ||= {}
498
+ @@callbacks[:error][args[:error]] = block
499
+ elsif args[:when]
500
+ @@callbacks[:when] ||= {}
501
+ @@callbacks[:when][args[:when]] = block
502
+ end
503
+ end
504
+
505
+ protected
506
+ def run_callback_for(args = {})
507
+ @@callbacks ||= {}
508
+ block = nil
509
+
510
+ if args[:mime_type]
511
+ return if @@callbacks[:mime_type].nil?
512
+ block = @@callbacks[:mime_type][args[:mime_type]]
513
+ elsif args[:error]
514
+ return if @@callbacks[:error].nil?
515
+ block = @@callbacks[:error][args[:error]]
516
+ elsif args[:when]
517
+ return if @@callbacks[:when].nil?
518
+ block = @@callbacks[:when][args[:when]]
519
+ end
520
+
521
+ self.instance_eval(&block) unless block.nil?
522
+ end
523
+
524
+ end
525
+
526
+ end
@@ -0,0 +1,51 @@
1
+ module S3
2
+
3
+ # All errors are derived from ServiceError. It's never actually raised itself, though.
4
+ class ServiceError < Exception; end
5
+
6
+ # A factory for building exception classes.
7
+ YAML::load(<<-END).
8
+ AccessDenied: [403, Access Denied]
9
+ AllAccessDisabled: [401, All access to this object has been disabled.]
10
+ AmbiguousGrantByEmailAddress: [400, The e-mail address you provided is associated with more than one account.]
11
+ BadAuthentication: [401, The authorization information you provided is invalid. Please try again.]
12
+ BadDigest: [400, The Content-MD5 you specified did not match what we received.]
13
+ BucketAlreadyExists: [409, The named bucket you tried to create already exists.]
14
+ BucketNotEmpty: [409, The bucket you tried to delete is not empty.]
15
+ CredentialsNotSupported: [400, This request does not support credentials.]
16
+ EntityTooLarge: [400, Your proposed upload exceeds the maximum allowed object size.]
17
+ IncompleteBody: [400, You did not provide the number of bytes specified by the Content-Length HTTP header.]
18
+ InternalError: [500, We encountered an internal error. Please try again.]
19
+ InvalidArgument: [400, Invalid Argument]
20
+ InvalidBucketName: [400, The specified bucket is not valid.]
21
+ InvalidDigest: [400, The Content-MD5 you specified was an invalid.]
22
+ InvalidRange: [416, The requested range is not satisfiable.]
23
+ InvalidSecurity: [403, The provided security credentials are not valid.]
24
+ InvalidSOAPRequest: [400, The SOAP request body is invalid.]
25
+ InvalidStorageClass: [400, The storage class you specified is not valid.]
26
+ InvalidURI: [400, Couldn't parse the specified URI.]
27
+ MalformedACLError: [400, The XML you provided was not well-formed or did not validate against our published schema.]
28
+ MethodNotAllowed: [405, The specified method is not allowed against this resource.]
29
+ MissingContentLength: [411, You must provide the Content-Length HTTP header.]
30
+ MissingSecurityElement: [400, The SOAP 1.1 request is missing a security element.]
31
+ MissingSecurityHeader: [400, Your request was missing a required header.]
32
+ NoSuchBucket: [404, The specified bucket does not exist.]
33
+ NoSuchKey: [404, The specified key does not exist.]
34
+ NotImplemented: [501, A header you provided implies functionality that is not implemented.]
35
+ NotModified: [304, The request resource has not been modified.]
36
+ PreconditionFailed: [412, At least one of the pre-conditions you specified did not hold.]
37
+ RequestTimeout: [400, Your socket connection to the server was not read from or written to within the timeout period.]
38
+ RequestTorrentOfBucketError: [400, Requesting the torrent file of a bucket is not permitted.]
39
+ TooManyBuckets: [400, You have attempted to create more buckets than allowed.]
40
+ UnexpectedContent: [400, This request does not support content.]
41
+ UnresolvableGrantByEmailAddress: [400, The e-mail address you provided does not match any account on record.]
42
+ END
43
+ each do |code, (status, msg)|
44
+ const_set(code, Class.new(ServiceError) {
45
+ {:code=>code, :status=>status, :message=>msg}.each do |k,v|
46
+ define_method(k) { v }
47
+ end
48
+ })
49
+ end
50
+
51
+ end
@@ -0,0 +1,20 @@
1
+ class Dir
2
+ def empty?
3
+ Dir.glob("#{ path }/*", File::FNM_DOTMATCH) do |e|
4
+ return false unless %w( . .. ).include?(File::basename(e))
5
+ end
6
+ return true
7
+ end
8
+ def self.empty? path
9
+ new(path).empty?
10
+ end
11
+ end
12
+
13
+ class String
14
+ def to_hex_s
15
+ unpack("H*").first
16
+ end
17
+ def from_hex_s
18
+ [self].pack("H*")
19
+ end
20
+ end
@@ -0,0 +1,100 @@
1
+ module S3
2
+ module Helpers
3
+ module ACP
4
+
5
+ # Kick out any users which do not have acp read access to a certain resource.
6
+ def only_can_read_acp bit; raise S3::AccessDenied unless bit.acp_readable_by? @user end
7
+ # Kick out any users which do not have acp write access to a certain resource.
8
+ def only_can_write_acp bit; raise S3::AccessDenied unless bit.acp_writable_by? @user end
9
+
10
+ def acl_response_for(bit)
11
+ only_can_read_acp(bit)
12
+
13
+ xml do |x|
14
+ x.AccessControlPolicy :xmlns => "http://s3.amazonaws.com/doc/2006-03-01/" do
15
+ x.Owner do
16
+ x.ID bit.owner.key
17
+ x.DisplayName bit.owner.login
18
+ end
19
+ x.AccessControlList do
20
+ bit.acl_list.each_pair do |key,acl|
21
+ x.Grant do
22
+ x.Grantee "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", "xsi:type" => acl[:type] do
23
+ if acl[:type] == "CanonicalUser"
24
+ x.ID acl[:id]
25
+ x.DisplayName acl[:name]
26
+ else
27
+ x.URI acl[:uri]
28
+ end
29
+ end
30
+ x.Permission acl[:access]
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def update_user_access(slot,user,access)
39
+ if slot.acl_list[user.key]
40
+ unless access == slot.acl_list[user.key][:access]
41
+ BitsUser.update_all("access = #{access}", ["bit_id = ? AND user_id = ?", slot.id, user.id ])
42
+ end
43
+ else
44
+ BitsUser.create(:bit_id => slot.id, :user_id => user.id, :access => access)
45
+ end
46
+ end
47
+
48
+ # Parse any ACL requests which have come in.
49
+ def requested_acl(slot=nil)
50
+ if slot && params.has_key?('acl')
51
+ only_can_write_acp slot
52
+ env['rack.input'].rewind
53
+ data = env['rack.input'].read
54
+ xml_request = REXML::Document.new(data).root
55
+ xml_request.each_element('//Grant') do |element|
56
+ new_perm = element.elements['Permission'].text
57
+ new_access = "#{Bit.acl_text.invert[new_perm]}00".to_i(8)
58
+ grantee = element.elements['Grantee']
59
+
60
+ case grantee.attributes["type"]
61
+ when "CanonicalUser"
62
+ user_check = User.find_by_key(grantee.elements["ID"].text)
63
+ unless user_check.nil? || slot.owner.id == user_check.id
64
+ update_user_access(slot,user_check,new_access)
65
+ end
66
+ when "Group"
67
+ if grantee.elements['URI'].text =~ /AuthenticatedUsers/
68
+ slot.access &= ~(slot.access.to_s(8)[1,1].to_i*10)
69
+ slot.access |= (Bit.acl_text.invert[new_perm]*10).to_s.to_i(8)
70
+ end
71
+ if grantee.elements['URI'].text =~ /AllUsers/
72
+ slot.access &= ~slot.access.to_s(8)[2,1].to_i
73
+ slot.access |= Bit.acl_text.invert[new_perm].to_s.to_i(8)
74
+ end
75
+ slot.save()
76
+ when "AmazonCustomerByEmail"
77
+ user_check = User.find_by_email(grantee.elements["EmailAddress"].text)
78
+ unless user_check.nil? || slot.owner.id == user_check.id
79
+ update_user_access(slot,user_check,new_access)
80
+ end
81
+ when ""
82
+ else
83
+ raise NotImplemented
84
+ end
85
+ end
86
+ {}
87
+ else
88
+ if @amz['acl'].nil?
89
+ access = slot.access unless slot.nil?
90
+ access ||= slot.parent.access unless slot.nil? || slot.parent.nil?
91
+ else
92
+ access = CANNED_ACLS[@amz['acl']]
93
+ end
94
+ { :access => access.nil? ? CANNED_ACLS['private'] : access }
95
+ end
96
+ end
97
+
98
+ end
99
+ end
100
+ end