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.
- data/README +23 -0
- data/Rakefile +51 -0
- data/bin/sinatra-s3 +30 -0
- data/db/migrate/001_create_bits.rb +28 -0
- data/db/migrate/002_create_users.rb +24 -0
- data/db/migrate/003_create_bits_users.rb +16 -0
- data/db/migrate/004_create_torrents.rb +22 -0
- data/db/migrate/005_create_torrent_peers.rb +26 -0
- data/examples/README +9 -0
- data/examples/wiki.rb +199 -0
- data/examples/wiki.ru +5 -0
- data/examples/wikicloth/MIT-LICENSE +20 -0
- data/examples/wikicloth/README +81 -0
- data/examples/wikicloth/Rakefile +23 -0
- data/examples/wikicloth/init.rb +1 -0
- data/examples/wikicloth/install.rb +0 -0
- data/examples/wikicloth/lib/core_ext.rb +43 -0
- data/examples/wikicloth/lib/wiki_buffer/html_element.rb +237 -0
- data/examples/wikicloth/lib/wiki_buffer/link.rb +70 -0
- data/examples/wikicloth/lib/wiki_buffer/table.rb +159 -0
- data/examples/wikicloth/lib/wiki_buffer/var.rb +77 -0
- data/examples/wikicloth/lib/wiki_buffer.rb +279 -0
- data/examples/wikicloth/lib/wiki_cloth.rb +61 -0
- data/examples/wikicloth/lib/wiki_link_handler.rb +138 -0
- data/examples/wikicloth/lib/wikicloth.rb +5 -0
- data/examples/wikicloth/run_tests.rb +48 -0
- data/examples/wikicloth/sample_documents/air_force_one.wiki +170 -0
- data/examples/wikicloth/sample_documents/cheatsheet.wiki +205 -0
- data/examples/wikicloth/sample_documents/default.css +34 -0
- data/examples/wikicloth/sample_documents/elements.wiki +7 -0
- data/examples/wikicloth/sample_documents/george_washington.wiki +526 -0
- data/examples/wikicloth/sample_documents/images.wiki +15 -0
- data/examples/wikicloth/sample_documents/lists.wiki +421 -0
- data/examples/wikicloth/sample_documents/pipe_trick.wiki +68 -0
- data/examples/wikicloth/sample_documents/random.wiki +55 -0
- data/examples/wikicloth/sample_documents/tv.wiki +312 -0
- data/examples/wikicloth/sample_documents/wiki.png +0 -0
- data/examples/wikicloth/sample_documents/wiki_tables.wiki +410 -0
- data/examples/wikicloth/tasks/wikicloth_tasks.rake +0 -0
- data/examples/wikicloth/test/test_helper.rb +3 -0
- data/examples/wikicloth/test/wiki_cloth_test.rb +8 -0
- data/examples/wikicloth/uninstall.rb +0 -0
- data/examples/wikicloth/wikicloth-0.1.3.gem +0 -0
- data/examples/wikicloth/wikicloth.gemspec +69 -0
- data/lib/sinatra-s3/admin.rb +626 -0
- data/lib/sinatra-s3/base.rb +526 -0
- data/lib/sinatra-s3/errors.rb +51 -0
- data/lib/sinatra-s3/ext.rb +20 -0
- data/lib/sinatra-s3/helpers/acp.rb +100 -0
- data/lib/sinatra-s3/helpers/admin.rb +41 -0
- data/lib/sinatra-s3/helpers/tracker.rb +42 -0
- data/lib/sinatra-s3/helpers/versioning.rb +27 -0
- data/lib/sinatra-s3/helpers.rb +79 -0
- data/lib/sinatra-s3/models/bit.rb +180 -0
- data/lib/sinatra-s3/models/bucket.rb +81 -0
- data/lib/sinatra-s3/models/file_info.rb +3 -0
- data/lib/sinatra-s3/models/git_bucket.rb +3 -0
- data/lib/sinatra-s3/models/slot.rb +47 -0
- data/lib/sinatra-s3/models/torrent.rb +6 -0
- data/lib/sinatra-s3/models/torrent_peer.rb +5 -0
- data/lib/sinatra-s3/models/user.rb +35 -0
- data/lib/sinatra-s3/s3.rb +57 -0
- data/lib/sinatra-s3/tasks.rb +62 -0
- data/lib/sinatra-s3/tracker.rb +134 -0
- data/lib/sinatra-s3.rb +1 -0
- data/public/css/control.css +225 -0
- data/public/css/wiki.css +47 -0
- data/public/images/external-link.gif +0 -0
- data/public/js/prototype.js +2539 -0
- data/public/js/upload_status.js +117 -0
- data/public/test.html +8 -0
- data/s3.yml.example +17 -0
- data/test/s3api_test.rb +121 -0
- data/test/test_helper.rb +25 -0
- 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
|