aws-sdk-resources 2.0.1.pre → 2.0.2.pre
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.
- checksums.yaml +4 -4
- data/lib/aws-sdk-resources.rb +6 -1
- data/lib/aws-sdk-resources/resource.rb +6 -5
- data/lib/aws-sdk-resources/s3.rb +41 -0
- data/lib/aws-sdk-resources/s3/file_part.rb +74 -0
- data/lib/aws-sdk-resources/s3/file_uploader.rb +58 -0
- data/lib/aws-sdk-resources/s3/multipart_file_uploader.rb +163 -0
- data/lib/aws-sdk-resources/s3/multipart_upload_error.rb +16 -0
- metadata +9 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d8d73ea1b21204559a8fb036e60f6d380268a743
|
4
|
+
data.tar.gz: b92f963d53c46d4dc64aaab930a5acea6bcba3b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cf73777ea642f80159f357fc2b0e6e1c935637c7688e650ba8949205a242517997a88e84527d02c78264caf37abf19cc92e69f3b510d42a5fca945fc9ff4dca0
|
7
|
+
data.tar.gz: ad3dd237c4c1ef3068794bfa1d2ec347f07e44880321a748f610f18037aa82cd57203984d808e431f8106ed4844ea61626cf1b78bd020eaf87da4158d1bb558d
|
data/lib/aws-sdk-resources.rb
CHANGED
@@ -4,11 +4,10 @@ module Aws
|
|
4
4
|
|
5
5
|
extend OperationMethods
|
6
6
|
|
7
|
-
# @overload initialize(options = {})
|
8
7
|
# @overload initialize(*identifiers, options = {})
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
8
|
+
# @param [Hash] options Options except `:data` and identifier options are
|
9
|
+
# used to construct a {Client} unless `:client` is given.
|
10
|
+
# @option options [Client] :client
|
12
11
|
def initialize(*args)
|
13
12
|
options = args.last.is_a?(Hash) ? args.pop.dup : {}
|
14
13
|
@identifiers = extract_identifiers(args, options)
|
@@ -62,7 +61,9 @@ module Aws
|
|
62
61
|
if options[:client]
|
63
62
|
options[:client]
|
64
63
|
else
|
65
|
-
self.class.client_class.new(options
|
64
|
+
self.class.client_class.new(options.merge(
|
65
|
+
user_agent_suffix: "resources"
|
66
|
+
))
|
66
67
|
end
|
67
68
|
end
|
68
69
|
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Aws
|
2
|
+
module S3
|
3
|
+
|
4
|
+
autoload :FilePart, 'aws-sdk-resources/s3/file_part'
|
5
|
+
autoload :FileUploader, 'aws-sdk-resources/s3/file_uploader'
|
6
|
+
autoload :MultipartFileUploader, 'aws-sdk-resources/s3/multipart_file_uploader'
|
7
|
+
autoload :MultipartUploadError, 'aws-sdk-resources/s3/multipart_upload_error'
|
8
|
+
|
9
|
+
class Object
|
10
|
+
|
11
|
+
# Uploads a file from disk to the current object in S3.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
#
|
15
|
+
# obj.upload_file('/path/to/file')
|
16
|
+
#
|
17
|
+
# @param [String,Pathname,File,Tempfile] source A file or path to a file
|
18
|
+
# on the local file system that should be uploaded to this object.
|
19
|
+
#
|
20
|
+
# @option options [Integer] :multipart_threshold (15728640) Files larger
|
21
|
+
# than `:multipart_threshold` are uploaded using the S3 multipart APIs.
|
22
|
+
# Default threshold is 15MB.
|
23
|
+
#
|
24
|
+
# @raise [MultipartUploadError] If an object is being uploaded in
|
25
|
+
# parts, and the upload can not be completed, then the upload is
|
26
|
+
# aborted and this error is raised. The raised error has a `#errors`
|
27
|
+
# method that returns the failures that caused the upload to be
|
28
|
+
# aborted.
|
29
|
+
#
|
30
|
+
# @return [void]
|
31
|
+
#
|
32
|
+
def upload_file(source, options = {})
|
33
|
+
uploader = FileUploader.new(
|
34
|
+
multipart_threshold: options.delete(:multipart_threshold),
|
35
|
+
client: client)
|
36
|
+
uploader.upload(source, options.merge(bucket: bucket_name, key: key))
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Aws
|
2
|
+
module S3
|
3
|
+
|
4
|
+
# A utility class that provides an IO-like interface to a portion of
|
5
|
+
# a file on disk.
|
6
|
+
# @api private
|
7
|
+
class FilePart
|
8
|
+
|
9
|
+
# @option options [required,String,Pathname,File,Tempfile] :source
|
10
|
+
# @option options [required,Integer] :offset The file part will read
|
11
|
+
# starting at this byte offset.
|
12
|
+
# @option options [required,Integer] :size The maximum number of bytes to
|
13
|
+
# read from the `:offset`.
|
14
|
+
def initialize(options = {})
|
15
|
+
@source = options[:source]
|
16
|
+
@first_byte = options[:offset]
|
17
|
+
@last_byte = @first_byte + options[:size]
|
18
|
+
@size = options[:size]
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [String,Pathname,File,Tempfile]
|
22
|
+
attr_reader :source
|
23
|
+
|
24
|
+
# @return [Integer]
|
25
|
+
attr_reader :first_byte
|
26
|
+
|
27
|
+
# @return [Integer]
|
28
|
+
attr_reader :last_byte
|
29
|
+
|
30
|
+
# @return [Integer]
|
31
|
+
attr_reader :size
|
32
|
+
|
33
|
+
def read(bytes = nil, output_buffer = nil)
|
34
|
+
open_file unless @file
|
35
|
+
read_from_file(bytes, output_buffer)
|
36
|
+
end
|
37
|
+
|
38
|
+
def rewind
|
39
|
+
if @file
|
40
|
+
@file.seek(@first_byte)
|
41
|
+
@position = @first_byte
|
42
|
+
end
|
43
|
+
0
|
44
|
+
end
|
45
|
+
|
46
|
+
def close
|
47
|
+
@file.close if @file
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def open_file
|
53
|
+
@file = File.open(@source, 'rb')
|
54
|
+
rewind
|
55
|
+
end
|
56
|
+
|
57
|
+
def read_from_file(bytes, output_buffer)
|
58
|
+
if bytes
|
59
|
+
data = @file.read([remaining_bytes, bytes].min)
|
60
|
+
data = nil if data == ''
|
61
|
+
else
|
62
|
+
data = @file.read(remaining_bytes)
|
63
|
+
end
|
64
|
+
@position += data ? data.bytesize : 0
|
65
|
+
output_buffer ? output_buffer.replace(data || '') : data
|
66
|
+
end
|
67
|
+
|
68
|
+
def remaining_bytes
|
69
|
+
@last_byte - @position
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Aws
|
4
|
+
module S3
|
5
|
+
# @api private
|
6
|
+
class FileUploader
|
7
|
+
|
8
|
+
FIFTEEN_MEGABYTES = 15 * 1024 * 1024
|
9
|
+
|
10
|
+
# @option options [Client] :client
|
11
|
+
# @option options [Integer] :multipart_threshold Files greater than
|
12
|
+
# `:multipart_threshold` bytes are uploaded using S3 multipart APIs.
|
13
|
+
def initialize(options = {})
|
14
|
+
@options = options
|
15
|
+
@client = options[:client] || Client.new
|
16
|
+
@multipart_threshold = options[:multipart_threshold] || FIFTEEN_MEGABYTES
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Client]
|
20
|
+
attr_reader :client
|
21
|
+
|
22
|
+
# @return [Integer] Files larger than this in bytes are uploaded
|
23
|
+
# using a {MultipartFileUploader}.
|
24
|
+
attr_reader :multipart_threshold
|
25
|
+
|
26
|
+
# @param [String,Pathname,File,Tempfile] source
|
27
|
+
# @option options [requried,String] :bucket
|
28
|
+
# @option options [requried,String] :key
|
29
|
+
# @return [void]
|
30
|
+
def upload(source, options = {})
|
31
|
+
if File.size(source) >= multipart_threshold
|
32
|
+
MultipartFileUploader.new(@options).upload(source, options)
|
33
|
+
else
|
34
|
+
put_object(source, options)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def put_object(source, options)
|
41
|
+
open_file(source) do |file|
|
42
|
+
@client.put_object(options.merge(body: file))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def open_file(source)
|
47
|
+
if String === source || Pathname === source
|
48
|
+
file = File.open(source, 'rb')
|
49
|
+
yield(file)
|
50
|
+
file.close
|
51
|
+
else
|
52
|
+
yield(source)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module Aws
|
5
|
+
module S3
|
6
|
+
# @api private
|
7
|
+
class MultipartFileUploader
|
8
|
+
|
9
|
+
MIN_PART_SIZE = 5 * 1024 * 1024 # 5MB
|
10
|
+
|
11
|
+
FILE_TOO_SMALL = "unable to multipart upload files smaller than 5MB"
|
12
|
+
|
13
|
+
MAX_PARTS = 10_000
|
14
|
+
|
15
|
+
THREAD_COUNT = 10
|
16
|
+
|
17
|
+
# @option options [Client] :client
|
18
|
+
def initialize(options = {})
|
19
|
+
@client = options[:client] || Client.new
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Client]
|
23
|
+
attr_reader :client
|
24
|
+
|
25
|
+
# @param [String,Pathname,File,Tempfile] source
|
26
|
+
# @option options [requried,String] :bucket
|
27
|
+
# @option options [requried,String] :key
|
28
|
+
# @return [void]
|
29
|
+
def upload(source, options = {})
|
30
|
+
if File.size(source) < MIN_PART_SIZE
|
31
|
+
raise ArgumentError, FILE_TOO_SMALL
|
32
|
+
else
|
33
|
+
upload_id = initiate_upload(options)
|
34
|
+
parts = upload_parts(upload_id, source, options)
|
35
|
+
complete_upload(upload_id, parts, options)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def initiate_upload(options)
|
42
|
+
@client.create_multipart_upload(options).upload_id
|
43
|
+
end
|
44
|
+
|
45
|
+
def complete_upload(upload_id, parts, options)
|
46
|
+
@client.complete_multipart_upload(
|
47
|
+
bucket: options[:bucket],
|
48
|
+
key: options[:key],
|
49
|
+
upload_id: upload_id,
|
50
|
+
multipart_upload: { parts: parts })
|
51
|
+
end
|
52
|
+
|
53
|
+
def upload_parts(upload_id, source, options)
|
54
|
+
pending = PartList.new(compute_parts(upload_id, source, options))
|
55
|
+
completed = PartList.new
|
56
|
+
errors = upload_in_threads(pending, completed)
|
57
|
+
if errors.empty?
|
58
|
+
completed.to_a.sort_by { |part| part[:part_number] }
|
59
|
+
else
|
60
|
+
abort_upload(upload_id, options, errors)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def abort_upload(upload_id, options, errors)
|
65
|
+
@client.abort_multipart_upload(
|
66
|
+
bucket: options[:bucket],
|
67
|
+
key: options[:key],
|
68
|
+
upload_id: upload_id
|
69
|
+
)
|
70
|
+
msg = "multipart upload failed: #{errors.map(&:message).join("; ")}"
|
71
|
+
raise MultipartUploadError.new(msg, errors)
|
72
|
+
rescue MultipartUploadError => error
|
73
|
+
raise error
|
74
|
+
rescue => error
|
75
|
+
msg = "failed to abort multipart upload: #{error.message}"
|
76
|
+
raise MultipartUploadError.new(msg, errors + [error])
|
77
|
+
end
|
78
|
+
|
79
|
+
def compute_parts(upload_id, source, options)
|
80
|
+
size = File.size(source)
|
81
|
+
default_part_size = compute_default_part_size(size)
|
82
|
+
offset = 0
|
83
|
+
part_number = 1
|
84
|
+
parts = []
|
85
|
+
while offset < size
|
86
|
+
parts << options.merge(
|
87
|
+
upload_id: upload_id,
|
88
|
+
part_number: part_number,
|
89
|
+
body: FilePart.new(
|
90
|
+
source: source,
|
91
|
+
offset: offset,
|
92
|
+
size: part_size(size, default_part_size, offset)
|
93
|
+
)
|
94
|
+
)
|
95
|
+
part_number += 1
|
96
|
+
offset += default_part_size
|
97
|
+
end
|
98
|
+
parts
|
99
|
+
end
|
100
|
+
|
101
|
+
def upload_in_threads(pending, completed)
|
102
|
+
threads = []
|
103
|
+
THREAD_COUNT.times do
|
104
|
+
thread = Thread.new do
|
105
|
+
begin
|
106
|
+
while part = pending.shift
|
107
|
+
resp = @client.upload_part(part)
|
108
|
+
part[:body].close
|
109
|
+
completed.push(etag: resp.etag, part_number: part[:part_number])
|
110
|
+
end
|
111
|
+
nil
|
112
|
+
rescue => error
|
113
|
+
# keep other threads from uploading other parts
|
114
|
+
pending.clear!
|
115
|
+
error
|
116
|
+
end
|
117
|
+
end
|
118
|
+
thread.abort_on_exception = true
|
119
|
+
threads << thread
|
120
|
+
end
|
121
|
+
errors = threads.map(&:value).compact
|
122
|
+
end
|
123
|
+
|
124
|
+
def compute_default_part_size(source_size)
|
125
|
+
[(source_size.to_f / MAX_PARTS).ceil, MIN_PART_SIZE].max.to_i
|
126
|
+
end
|
127
|
+
|
128
|
+
def part_size(total_size, part_size, offset)
|
129
|
+
if offset + part_size > total_size
|
130
|
+
total_size - offset
|
131
|
+
else
|
132
|
+
part_size
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# @api private
|
137
|
+
class PartList
|
138
|
+
|
139
|
+
def initialize(parts = [])
|
140
|
+
@parts = parts
|
141
|
+
@mutex = Mutex.new
|
142
|
+
end
|
143
|
+
|
144
|
+
def push(part)
|
145
|
+
@mutex.synchronize { @parts.push(part) }
|
146
|
+
end
|
147
|
+
|
148
|
+
def shift
|
149
|
+
@mutex.synchronize { @parts.shift }
|
150
|
+
end
|
151
|
+
|
152
|
+
def clear!
|
153
|
+
@mutex.synchronize { @parts.clear }
|
154
|
+
end
|
155
|
+
|
156
|
+
def to_a
|
157
|
+
@mutex.synchronize { @parts.dup }
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Aws
|
2
|
+
module S3
|
3
|
+
class MultipartUploadError < StandardError
|
4
|
+
|
5
|
+
def initialize(message, errors)
|
6
|
+
@errors = errors
|
7
|
+
super(message)
|
8
|
+
end
|
9
|
+
|
10
|
+
# @return [Array<StandardError>] The list of errors encountered
|
11
|
+
# when uploading or aborting the upload.
|
12
|
+
attr_reader :errors
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aws-sdk-resources
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.2.pre
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Amazon Web Services
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-10-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk-core
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - '='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 2.0.
|
19
|
+
version: 2.0.2
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 2.0.
|
26
|
+
version: 2.0.2
|
27
27
|
description: Provides resource-oriented abstractions for AWS.
|
28
28
|
email:
|
29
29
|
executables: []
|
@@ -51,6 +51,11 @@ files:
|
|
51
51
|
- lib/aws-sdk-resources/request.rb
|
52
52
|
- lib/aws-sdk-resources/request_params.rb
|
53
53
|
- lib/aws-sdk-resources/resource.rb
|
54
|
+
- lib/aws-sdk-resources/s3/file_part.rb
|
55
|
+
- lib/aws-sdk-resources/s3/file_uploader.rb
|
56
|
+
- lib/aws-sdk-resources/s3/multipart_file_uploader.rb
|
57
|
+
- lib/aws-sdk-resources/s3/multipart_upload_error.rb
|
58
|
+
- lib/aws-sdk-resources/s3.rb
|
54
59
|
- lib/aws-sdk-resources/source.rb
|
55
60
|
- lib/aws-sdk-resources/validator/context.rb
|
56
61
|
- lib/aws-sdk-resources/validator/identifier_validator.rb
|
@@ -84,4 +89,3 @@ signing_key:
|
|
84
89
|
specification_version: 4
|
85
90
|
summary: AWS SDK for Ruby - Resources
|
86
91
|
test_files: []
|
87
|
-
has_rdoc:
|