shuck 0.0.7 → 0.0.8

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.
@@ -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