shuck 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- shuck (0.0.6)
4
+ shuck (0.0.8)
5
5
  builder
6
6
  thor
7
7
 
@@ -13,17 +13,10 @@ GEM
13
13
  mime-types
14
14
  xml-simple
15
15
  builder (2.1.2)
16
- columnize (0.3.2)
17
- linecache (0.43)
18
16
  mime-types (1.16)
19
17
  right_aws (2.0.0)
20
18
  right_http_connection (>= 1.2.1)
21
19
  right_http_connection (1.2.4)
22
- ruby-debug (0.10.4)
23
- columnize (>= 0.1)
24
- ruby-debug-base (~> 0.10.4.0)
25
- ruby-debug-base (0.10.4)
26
- linecache (>= 0.3)
27
20
  thor (0.14.4)
28
21
  xml-simple (1.0.12)
29
22
 
@@ -35,6 +28,5 @@ DEPENDENCIES
35
28
  builder
36
29
  bundler (>= 1.0.0)
37
30
  right_aws
38
- ruby-debug
39
31
  shuck!
40
32
  thor
data/Rakefile CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'bundler'
2
2
  require 'rake/testtask'
3
+ include Rake::DSL
3
4
  Bundler::GemHelper.install_tasks
4
5
 
5
6
  Rake::TestTask.new(:test) do |t|
@@ -9,12 +9,14 @@ module Shuck
9
9
  method_option :root, :type => :string, :aliases => '-r', :required => true
10
10
  method_option :port, :type => :numeric, :aliases => '-p', :required => true
11
11
  method_option :hostname, :type => :string, :aliases => '-h', :desc => "The root name of the host. Defaults to localhost."
12
+ method_option :limit, :aliases => '-l', :type => :string, :desc => 'Rate limit for serving (ie. 50K, 1.0M)'
12
13
  def server
13
14
  store = nil
14
15
  if options[:root]
15
16
  root = File.expand_path(options[:root])
16
17
  store = FileStore.new(root)
17
18
  end
19
+
18
20
  if store.nil?
19
21
  puts "You must specify a root to use a file store (the current default)"
20
22
  exit(-1)
@@ -25,6 +27,15 @@ module Shuck
25
27
  hostname = options[:hostname]
26
28
  end
27
29
 
30
+ if options[:limit]
31
+ begin
32
+ store.rate_limit = options[:limit]
33
+ rescue
34
+ puts $!.message
35
+ exit(-1)
36
+ end
37
+ end
38
+
28
39
  puts "Loading Shuck with #{root} on port #{options[:port]} with hostname #{hostname}"
29
40
  server = Shuck::Server.new(options[:port],store,hostname)
30
41
  server.serve
@@ -2,6 +2,7 @@ require 'fileutils'
2
2
  require 'time'
3
3
  require 'shuck/s3_object'
4
4
  require 'shuck/bucket'
5
+ require 'shuck/rate_limitable_file'
5
6
  require 'digest/md5'
6
7
  require 'yaml'
7
8
 
@@ -21,6 +22,25 @@ module Shuck
21
22
  end
22
23
  end
23
24
 
25
+ # Pass a rate limit in bytes per second
26
+ def rate_limit=(rate_limit)
27
+ if rate_limit.is_a?(String)
28
+ if rate_limit =~ /^(\d+)$/
29
+ RateLimitableFile.rate_limit = rate_limit.to_i
30
+ elsif rate_limit =~ /^(.*)K$/
31
+ RateLimitableFile.rate_limit = $1.to_f * 1000
32
+ elsif rate_limit =~ /^(.*)M$/
33
+ RateLimitableFile.rate_limit = $1.to_f * 1000000
34
+ elsif rate_limit =~ /^(.*)G$/
35
+ RateLimitableFile.rate_limit = $1.to_f * 1000000000
36
+ else
37
+ raise "Invalid Rate Limit Format: Valid values include (1000,10K,1.1M)"
38
+ end
39
+ else
40
+ RateLimitableFile.rate_limit = nil
41
+ end
42
+ end
43
+
24
44
  def buckets
25
45
  @buckets
26
46
  end
@@ -37,14 +57,15 @@ module Shuck
37
57
  end
38
58
 
39
59
  def get_object(bucket,object, request)
40
- begin
60
+ begin
41
61
  real_obj = S3Object.new
42
62
  obj_root = File.join(@root,bucket,object,SHUCK_METADATA_DIR)
43
63
  metadata = YAML.parse(File.open(File.join(obj_root,"metadata"),'rb').read)
44
64
  real_obj.name = object
45
65
  real_obj.md5 = metadata[:md5].value
46
66
  real_obj.content_type = metadata[:content_type] ? metadata[:content_type].value : "application/octet-stream"
47
- real_obj.io = File.open(File.join(obj_root,"content"),'rb')
67
+ #real_obj.io = File.open(File.join(obj_root,"content"),'rb')
68
+ real_obj.io = RateLimitableFile.open(File.join(obj_root,"content"),'rb')
48
69
  return real_obj
49
70
  rescue
50
71
  puts $!
@@ -55,6 +76,40 @@ module Shuck
55
76
  def object_metadata(bucket,object)
56
77
  end
57
78
 
79
+ def copy_object(src_bucket,src_object,dst_bucket,dst_object)
80
+ src_root = File.join(@root,src_bucket,src_object,SHUCK_METADATA_DIR)
81
+ src_obj = S3Object.new
82
+ src_metadata_filename = File.join(src_root,"metadata")
83
+ src_metadata = YAML.parse(File.open(src_metadata_filename,'rb').read)
84
+ src_content_filename = File.join(src_root,"content")
85
+
86
+ dst_filename= File.join(@root,dst_bucket,dst_object)
87
+ FileUtils.mkdir_p(dst_filename)
88
+
89
+ metadata_dir = File.join(dst_filename,SHUCK_METADATA_DIR)
90
+ FileUtils.mkdir_p(metadata_dir)
91
+
92
+ content = File.join(metadata_dir,"content")
93
+ metadata = File.join(metadata_dir,"metadata")
94
+
95
+ File.open(content,'wb') do |f|
96
+ File.open(src_content_filename,'rb') do |input|
97
+ f << input.read
98
+ end
99
+ end
100
+
101
+ File.open(metadata,'w') do |f|
102
+ File.open(src_metadata_filename,'r') do |input|
103
+ f << input.read
104
+ end
105
+ end
106
+
107
+ obj = S3Object.new
108
+ obj.md5 = src_metadata[:md5]
109
+ obj.content_type = src_metadata[:content_type]
110
+ return obj
111
+ end
112
+
58
113
  def store_object(bucket,object,request)
59
114
  begin
60
115
  filename = File.join(@root,bucket,object)
@@ -67,7 +122,8 @@ module Shuck
67
122
  metadata = File.join(filename,SHUCK_METADATA_DIR,"metadata")
68
123
 
69
124
  md5 = Digest::MD5.new
70
- File.open(content,'w') do |f|
125
+
126
+ File.open(content,'wb') do |f|
71
127
  request.body do |chunk|
72
128
  f << chunk
73
129
  md5 << chunk
@@ -0,0 +1,21 @@
1
+ module Shuck
2
+ class RateLimitableFile < File
3
+ @@rate_limit = nil
4
+ # Specify a rate limit in bytes per second
5
+ def self.rate_limit
6
+ @@rate_limit
7
+ end
8
+
9
+ def self.rate_limit=(rate_limit)
10
+ @@rate_limit = rate_limit
11
+ end
12
+
13
+ def read(args)
14
+ if @@rate_limit
15
+ time_to_sleep = args / @@rate_limit
16
+ sleep(time_to_sleep)
17
+ end
18
+ return super(args)
19
+ end
20
+ end
21
+ end
@@ -3,6 +3,28 @@ require 'shuck/file_store'
3
3
  require 'shuck/xml_adapter'
4
4
 
5
5
  module Shuck
6
+ class Request
7
+ CREATE_BUCKET = "CREATE_BUCKET"
8
+ STORE = "STORE"
9
+ COPY = "COPY"
10
+ GET = "GET"
11
+ MOVE = "MOVE"
12
+ DELETE = "DELETE"
13
+
14
+ attr_accessor :bucket,:object,:type,:src_bucket,:src_object,:method,:webrick_request,:path
15
+
16
+ def inspect
17
+ puts "-----Inspect Shuck Request"
18
+ puts "Type: #{@type}"
19
+ puts "Request Method: #{@method}"
20
+ puts "Bucket: #{@bucket}"
21
+ puts "Object: #{@object}"
22
+ puts "Src Bucket: #{@src_bucket}"
23
+ puts "Src Object: #{@src_object}"
24
+ puts "-----Done"
25
+ end
26
+ end
27
+
6
28
  class Servlet < WEBrick::HTTPServlet::AbstractServlet
7
29
  def initialize(server,store,hostname)
8
30
  super(server)
@@ -34,16 +56,22 @@ module Shuck
34
56
  elems = path.split("/")
35
57
  end
36
58
 
37
- if elems.size == 1
59
+ if elems.size == 0
60
+ # List buckets
61
+ buckets = @store.buckets
62
+ response.status = 200
63
+ response.body = XmlAdapter.buckets(buckets)
64
+ response['Content-Type'] = "application/xml"
65
+ elsif elems.size == 1
38
66
  bucket_obj = @store.get_bucket(bucket)
39
67
  if bucket_obj
40
68
  response.status = 200
41
69
  response.body = XmlAdapter.bucket(bucket_obj)
42
- response['Content-Type'] = "application/xml"
70
+ response['Content-Type'] = "application/xml"
43
71
  else
44
72
  response.status = 404
45
73
  response.body = XmlAdapter.error_no_such_bucket(bucket)
46
- response['Content-Type'] = "application/xml"
74
+ response['Content-Type'] = "application/xml"
47
75
  end
48
76
  else
49
77
  object = elems[1,elems.size].join('/')
@@ -88,61 +116,97 @@ module Shuck
88
116
  end
89
117
 
90
118
  def do_PUT(request,response)
91
- path = request.path
119
+ s_req = normalize_request(request)
120
+
121
+ case s_req.type
122
+ when Request::COPY
123
+ @store.copy_object(s_req.src_bucket,s_req.src_object,s_req.bucket,s_req.object)
124
+ when Request::STORE
125
+ real_obj = @store.store_object(s_req.bucket,s_req.object,s_req.webrick_request)
126
+ response['Etag'] = real_obj.md5
127
+ when Request::CREATE_BUCKET
128
+ @store.create_bucket(s_req.bucket)
129
+ end
130
+
131
+ response.status = 200
132
+ response.body = ""
133
+ response['Content-Type'] = "text/xml"
134
+ end
135
+
136
+ def do_POST(request,response)
137
+ p request
138
+ end
139
+
140
+ def do_DELETE(request,response)
141
+ p request
142
+ end
143
+
144
+ private
145
+ # This method takes a webrick request and generates a normalized Shuck request
146
+ def normalize_request(webrick_r)
147
+ path = webrick_r.path
92
148
  path_len = path.size
93
149
  path_style_request = true
94
- host = request["Host"]
95
- puts host
150
+ host = webrick_r["Host"]
96
151
  bucket = nil
152
+ object = nil
97
153
  path_style_request = true
154
+
98
155
  if host != @hostname
99
156
  bucket = host.split(".")[0]
100
157
  path_style_request = false
101
158
  end
102
159
 
160
+ req = Request.new
161
+ req.path = webrick_r.path
162
+
103
163
  if path == "/"
104
164
  if bucket
105
- @store.create_bucket(bucket)
106
- else
107
- puts "Unsure what to do"
165
+ req.type = Request::CREATE_BUCKET
108
166
  end
109
- else
167
+ else
110
168
  if path_style_request
111
169
  elems = path[1,path_len].split("/")
112
170
  bucket = elems[0]
113
- else
114
- elems = path.split("/")
115
- end
116
-
117
- if path_style_request
118
- if elems.size == 1
119
- @store.create_bucket(bucket)
120
- else
171
+ if elems.size == 1
172
+ req.type = Request::CREATE_BUCKET
173
+ else
174
+ req.type = Request::STORE
121
175
  object = elems[1,elems.size].join('/')
122
- real_obj = @store.store_object(bucket,object,request)
123
- response['Etag'] = real_obj.md5
124
176
  end
125
177
  else
126
- real_obj = @store.store_object(bucket,path,request)
127
- response['Etag'] = real_obj.md5
178
+ req.type = Request::STORE
179
+ object = webrick_r.path
128
180
  end
129
181
  end
130
- response.status = 200
131
- response.body = ""
132
- response['Content-Type'] = "text/xml"
133
- end
134
182
 
135
- def do_POST(request,response)
136
- puts "Put"
137
- p request
183
+ copy_source = webrick_r.header["x-amz-copy-source"]
184
+ if copy_source and copy_source.size == 1
185
+ src_elems = copy_source.first.split("/")
186
+ root_offset = src_elems[0] == "" ? 1 : 0
187
+ req.src_bucket = src_elems[root_offset]
188
+ req.src_object = src_elems[1 + root_offset,src_elems.size].join("/")
189
+ req.type = Request::COPY
190
+ end
191
+
192
+ req.bucket = bucket
193
+ req.object = object
194
+ req.webrick_request = webrick_r
195
+ return req
138
196
  end
139
197
 
140
- def do_DELETE(request,response)
141
- puts "Delete"
142
- p request
198
+ def dump_request(request)
199
+ puts "----------Dump Request-------------"
200
+ puts request.request_method
201
+ puts request.path
202
+ request.each do |k,v|
203
+ puts "#{k}:#{v}"
204
+ end
205
+ puts "----------End Dump -------------"
143
206
  end
144
207
  end
145
208
 
209
+
146
210
  class Server
147
211
  def initialize(port,store,hostname = "localhost")
148
212
  @port = port
@@ -1,3 +1,3 @@
1
1
  module Shuck
2
- VERSION = "0.0.7"
2
+ VERSION = "0.0.8"
3
3
  end
@@ -3,7 +3,7 @@ require 'time'
3
3
 
4
4
  module Shuck
5
5
  class XmlAdapter
6
- def self.buckets(buckets)
6
+ def self.buckets(bucket_objects)
7
7
  output = ""
8
8
  xml = Builder::XmlMarkup.new(:target => output)
9
9
  xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
@@ -13,10 +13,10 @@ module Shuck
13
13
  owner.DisplayName("Shuck")
14
14
  }
15
15
  lam.Buckets { |buckets|
16
- @buckets.each do |bucket|
16
+ bucket_objects.each do |bucket|
17
17
  buckets.Bucket do |b|
18
18
  b.Name(bucket.name)
19
- b.CreationDate(bucket.creation_date.xmlschema)
19
+ b.CreationDate(bucket.creation_date.strftime("%Y-%m-%dT%H:%M:%S.000Z"))
20
20
  end
21
21
  end
22
22
  }
@@ -17,7 +17,8 @@ Gem::Specification.new do |s|
17
17
  s.add_development_dependency "bundler", ">= 1.0.0"
18
18
  s.add_development_dependency "aws-s3"
19
19
  s.add_development_dependency "right_aws"
20
- s.add_development_dependency "ruby-debug"
20
+ #s.add_development_dependency "aws-sdk"
21
+ #s.add_development_dependency "ruby-debug19"
21
22
  s.add_dependency "thor"
22
23
  s.add_dependency "builder"
23
24
 
@@ -0,0 +1,34 @@
1
+ [default]
2
+ access_key = abc
3
+ acl_public = False
4
+ bucket_location = US
5
+ cloudfront_host = cloudfront.amazonaws.com
6
+ cloudfront_resource = /2008-06-30/distribution
7
+ default_mime_type = binary/octet-stream
8
+ delete_removed = False
9
+ dry_run = False
10
+ encoding = UTF-8
11
+ encrypt = False
12
+ force = False
13
+ get_continue = False
14
+ gpg_command = None
15
+ gpg_decrypt = %(gpg_command)s -d --verbose --no-use-agent --batch --yes --passphrase-fd %(passphrase_fd)s -o %(output_file)s %(input_file)s
16
+ gpg_encrypt = %(gpg_command)s -c --verbose --no-use-agent --batch --yes --passphrase-fd %(passphrase_fd)s -o %(output_file)s %(input_file)s
17
+ gpg_passphrase =
18
+ guess_mime_type = True
19
+ host_base = s3.localhost:10453
20
+ host_bucket = %(bucket)s.s3.localhost:10453
21
+ human_readable_sizes = False
22
+ list_md5 = False
23
+ preserve_attrs = True
24
+ progress_meter = True
25
+ proxy_host =
26
+ proxy_port = 0
27
+ recursive = False
28
+ recv_chunk = 4096
29
+ secret_key = def
30
+ send_chunk = 4096
31
+ simpledb_host = sdb.amazonaws.com
32
+ skip_existing = False
33
+ use_https = False
34
+ verbosity = WARNING
@@ -6,7 +6,9 @@ require 'right_aws'
6
6
  class RightAWSCommandsTest < Test::Unit::TestCase
7
7
 
8
8
  def setup
9
- @s3 = RightAws::S3Interface.new('1E3GDYEOGFJPIT7XXXXXX','hgTHt68JY07JKUY08ftHYtERkjgtfERn57XXXXXX', {:multi_thread => false, :server => 'localhost', :port => 10453, :protocol => 'http' })
9
+ @s3 = RightAws::S3Interface.new('1E3GDYEOGFJPIT7XXXXXX','hgTHt68JY07JKUY08ftHYtERkjgtfERn57XXXXXX',
10
+ {:multi_thread => false, :server => 'localhost',
11
+ :port => 10453, :protocol => 'http',:logger => Logger.new("/dev/null") })
10
12
  end
11
13
 
12
14
  def teardown
@@ -51,4 +53,11 @@ class RightAWSCommandsTest < Test::Unit::TestCase
51
53
  assert_equal "recursive", output
52
54
  end
53
55
 
56
+ def test_intra_bucket_copy
57
+ @s3.put("s3media","original.txt","Hello World")
58
+ @s3.copy("s3media","original.txt","s3media","copy.txt")
59
+ obj = @s3.get("s3media","copy.txt")
60
+ assert_equal "Hello World",obj[:object]
61
+ end
62
+
54
63
  end
@@ -0,0 +1,30 @@
1
+ require 'test/test_helper'
2
+ require 'fileutils'
3
+ require 'shuck/server'
4
+
5
+ # You need to have s3cmd installed to use this
6
+ class S3CmdTest < Test::Unit::TestCase
7
+
8
+ def setup
9
+ @s3cmd = "s3cmd --config"
10
+ end
11
+
12
+ def teardown
13
+ end
14
+
15
+ def test_create_bucket
16
+ end
17
+
18
+ def test_store
19
+ end
20
+
21
+ def test_large_store
22
+ end
23
+
24
+ def test_multi_directory
25
+ end
26
+
27
+ def test_intra_bucket_copy
28
+ end
29
+
30
+ end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 0
8
- - 7
9
- version: 0.0.7
8
+ - 8
9
+ version: 0.0.8
10
10
  platform: ruby
11
11
  authors:
12
12
  - Curtis Spencer
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-06-06 00:00:00 -07:00
17
+ date: 2012-02-22 00:00:00 -08:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -59,7 +59,7 @@ dependencies:
59
59
  type: :development
60
60
  version_requirements: *id003
61
61
  - !ruby/object:Gem::Dependency
62
- name: ruby-debug
62
+ name: thor
63
63
  prerelease: false
64
64
  requirement: &id004 !ruby/object:Gem::Requirement
65
65
  none: false
@@ -69,10 +69,10 @@ dependencies:
69
69
  segments:
70
70
  - 0
71
71
  version: "0"
72
- type: :development
72
+ type: :runtime
73
73
  version_requirements: *id004
74
74
  - !ruby/object:Gem::Dependency
75
- name: thor
75
+ name: builder
76
76
  prerelease: false
77
77
  requirement: &id005 !ruby/object:Gem::Requirement
78
78
  none: false
@@ -84,19 +84,6 @@ dependencies:
84
84
  version: "0"
85
85
  type: :runtime
86
86
  version_requirements: *id005
87
- - !ruby/object:Gem::Dependency
88
- name: builder
89
- prerelease: false
90
- requirement: &id006 !ruby/object:Gem::Requirement
91
- none: false
92
- requirements:
93
- - - ">="
94
- - !ruby/object:Gem::Version
95
- segments:
96
- - 0
97
- version: "0"
98
- type: :runtime
99
- version_requirements: *id006
100
87
  description: Use Shuck to test basic S3 functionality without actually connecting to S3
101
88
  email:
102
89
  - curtis@sevenforge.com
@@ -117,13 +104,16 @@ files:
117
104
  - lib/shuck/bucket.rb
118
105
  - lib/shuck/cli.rb
119
106
  - lib/shuck/file_store.rb
107
+ - lib/shuck/rate_limitable_file.rb
120
108
  - lib/shuck/s3_object.rb
121
109
  - lib/shuck/server.rb
122
110
  - lib/shuck/version.rb
123
111
  - lib/shuck/xml_adapter.rb
124
112
  - shuck.gemspec
113
+ - test/local_s3_cfg
125
114
  - test/right_aws_commands_test.rb
126
115
  - test/s3_commands_test.rb
116
+ - test/s3cmd_test.rb
127
117
  - test/test_helper.rb
128
118
  has_rdoc: true
129
119
  homepage: ""
@@ -158,6 +148,8 @@ signing_key:
158
148
  specification_version: 3
159
149
  summary: Shuck is a phony S3 engine with minimal dependencies
160
150
  test_files:
151
+ - test/local_s3_cfg
161
152
  - test/right_aws_commands_test.rb
162
153
  - test/s3_commands_test.rb
154
+ - test/s3cmd_test.rb
163
155
  - test/test_helper.rb