s3_data_packer 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: da63067cb5437094daab442088507425866787ad176a98a0f7e515ae54f142fe
4
+ data.tar.gz: 249ffbb44f8e245c8383e17c1c6ae754e477935e8b461ed9193b8a12beb4394f
5
+ SHA512:
6
+ metadata.gz: 7f204a257f1b92d40e2e425b62e66e88238f06205e2da40940243cd086ce66005eb00ed25d3326bb25b6ee65351ca46a85165f7117d8526ec9f9de22a2cff165
7
+ data.tar.gz: 9c6681710e72702cb85fa63bf91a891afc32771626449f5dfc314ebd2f69fd8a52a279d0d1d889a15b4a016e9695ad2846f3eb395244df17f45ed7d5b5e4e0bf
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ log/*.log*
10
+ *~
11
+ *#*#*
12
+ .DS_Store
13
+ coverage/*
14
+ *.swp
15
+ *.iml
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ s3_data_packer
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.5.9
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.5.9
7
+ before_install: gem install bundler -v 1.16.6
data/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # v0.2.1
2
+
3
+ - Adding custom sources/targets wrappers
4
+ - More testing
5
+
6
+ # Unreleased
7
+
8
+ - First commit
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at rayko.drg@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in s3_data_packer.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,54 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ s3_data_packer (0.2.0)
5
+ aws-sdk-s3 (~> 1)
6
+ mime-types (~> 3)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ aws-eventstream (1.1.1)
12
+ aws-partitions (1.492.0)
13
+ aws-sdk-core (3.119.1)
14
+ aws-eventstream (~> 1, >= 1.0.2)
15
+ aws-partitions (~> 1, >= 1.239.0)
16
+ aws-sigv4 (~> 1.1)
17
+ jmespath (~> 1.0)
18
+ aws-sdk-kms (1.47.0)
19
+ aws-sdk-core (~> 3, >= 3.119.0)
20
+ aws-sigv4 (~> 1.1)
21
+ aws-sdk-s3 (1.100.0)
22
+ aws-sdk-core (~> 3, >= 3.119.0)
23
+ aws-sdk-kms (~> 1)
24
+ aws-sigv4 (~> 1.1)
25
+ aws-sigv4 (1.2.4)
26
+ aws-eventstream (~> 1, >= 1.0.2)
27
+ byebug (11.1.3)
28
+ docile (1.4.0)
29
+ jmespath (1.4.0)
30
+ mime-types (3.3.1)
31
+ mime-types-data (~> 3.2015)
32
+ mime-types-data (3.2021.0704)
33
+ minitest (5.14.4)
34
+ rake (10.5.0)
35
+ simplecov (0.21.2)
36
+ docile (~> 1.1)
37
+ simplecov-html (~> 0.11)
38
+ simplecov_json_formatter (~> 0.1)
39
+ simplecov-html (0.12.3)
40
+ simplecov_json_formatter (0.1.3)
41
+
42
+ PLATFORMS
43
+ ruby
44
+
45
+ DEPENDENCIES
46
+ bundler (~> 1.16)
47
+ byebug
48
+ minitest (~> 5.0)
49
+ rake (~> 10.0)
50
+ s3_data_packer!
51
+ simplecov
52
+
53
+ BUNDLED WITH
54
+ 1.16.6
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Rayko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,258 @@
1
+ # S3DataPacker
2
+
3
+ This small packer will read a large amount of individual files on an S3 location that represent single
4
+ items in JSON format, and pack them into larget batches with the option of compressing the final batch,
5
+ decreasing the total storage size of the data (if compressed), and also reducing the total number of files.
6
+
7
+ The idea is to prepare data dumped on S3 in this way to a more optimal layout for AWS Athena to setup
8
+ a querying system on top of it.
9
+
10
+ For now, S3DataPacker supports JSON items, with a 1 item per file layout, GZip compression if enabled, and
11
+ only from S3 to S3, though the source and target bucket can be different buckets or even on different accounts
12
+ if the proper credentials are provided.
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 's3_data_packer'
20
+ ```
21
+
22
+ Or use the `main` branch from repo:
23
+
24
+ ```ruby
25
+ gem 's3_data_packer', git: 'https://github.com/rayko/s3_data_packer.git', branch: 'main'
26
+ ```
27
+
28
+ And then execute:
29
+
30
+ $ bundle
31
+
32
+ Or install it yourself as:
33
+
34
+ $ gem install s3_data_packer
35
+
36
+ ## Configurations
37
+
38
+ There's a good number of options that can alter how the data is consumed. Below is the list of all defaults
39
+ out of the box:
40
+
41
+ ```
42
+ S3DataPacker.configure do |config|
43
+ config.logger = Logger.new('log/s3_data_packer.log') # Standard logger for information
44
+ config.thread_count = 2 # How many threads to run
45
+ config.thread_sleep_time = 1 # How long to wait when there's no work in queue
46
+ config.thread_lock_wait_time = 1 # How long to wait when a lock error happens before retrying
47
+ config.max_queue_size = 10000 # How big can the queue get during processing
48
+ config.max_queue_wait = 5 # How long to wait when the queue reached max_queue_size before continuing
49
+ config.workdir = 'tmp/s3_data_packer' # Where to keep output files until pushing to target location
50
+ config.cleanup_batch = true # Whether to remove the pushed batches or not
51
+ config.compress_batch = true # Whether to compresss with GZip or not
52
+ config.batch_size = 100000 # How many items to fit in a batch
53
+ config.s3_api_key = nil # Default API Key for an Aws account
54
+ config.s3_api_secret = nil # Default API Secret for an AWS account
55
+ config.s3_region = nil # Default region for the buckets to use
56
+ config.output_filename_prefix = nil # Static prefix to append on output filenames
57
+ config.output_filename_suffix = 'batch' # Static suffix to insert on output filenames
58
+ config.output_filename_pattern = %i[timecode_int suffix] # Simple pattern to construct output filenames (more on that below)
59
+ config.output_filename_splitter = '_' # Character to join elements into a string that'll be a final filename
60
+ end
61
+ ```
62
+
63
+ ### S3 Credentials
64
+
65
+ There are 2 main ways to do this depending on the context. Buckets can be configured in place with user provided
66
+ credentials for both source and target locations.
67
+
68
+ If the source and target locations are on the same account, region and use the same credentials, the options
69
+ above can be set to always set those credentials.
70
+
71
+ AWS credentials in the configuration here are optional, and just a shortcut to setting credentials for each
72
+ run.
73
+
74
+ ### Thread options
75
+
76
+ Various thread options are available to moderate how the process run. Depending on the hardware available
77
+ the thread counts can be adjusted to speed up the process. However, it there are enough threads, the queue
78
+ might run empty too soon, in which case threads will sleep the given ammount of time to wait to gather
79
+ some items to work on.
80
+
81
+ All timming settings should be adjusted depending on where this is going to run and the resources available.
82
+
83
+ ### Output filename options
84
+
85
+ There are a couple parameters that can be configured generally to generate filenames consistently. The simplest
86
+ options `:output_filename_prefix`, `:output_filename_suffix` and `:output_filename_splitter` are straight
87
+ forward. The `:output_filename_pattern` option is a bit more involved. It basically dictates order and what
88
+ values to use when generating a filename. When a new name needs to be generated, each item in the pattern will
89
+ be translated to a value of some kind, and merged toghether with the `:output_filename_splitter` character.
90
+ The contents of the pattern array must be `Symbol` names and can only be one of the following:
91
+
92
+ - :timecode_int -> current standard time in seconds (`Time.now.to_i`)
93
+ - :timecode_dec -> current standard time with milliseconds (`Time.now.to_f`)
94
+ - :number -> a simple number that grows as new names are generated
95
+ - :timestamp -> simple time stamp with format YYYYMMDDhhmmss
96
+ - :datestamp -> simple date stamp with format YYYYMMDD
97
+ - :prefix -> given static string to use as prefix on the name
98
+ - :suffix -> given static string to use as suffix on the name
99
+
100
+ Different patterns will generate different names with same structuring. The important part here is to always
101
+ include a variable element so final files do not override previous data.
102
+
103
+ A few examples of different patterns, setting prefix as 'data' and suffix as 'batch':
104
+
105
+ - [:timecode_int, :suffix] -> 1111111111_batch 1111111112_batch 1111111113_batch ...
106
+ - [:datestamp, :number] -> 20200101_1 20200101_2 20200101_3 ...
107
+ - [:prefix, :number, :suffix] -> data_1_batch data_2_batch data_3_batch ...
108
+
109
+ ## Usage
110
+
111
+ The simplest setup for this simple file processor is to set the AWS credentials and region through the
112
+ configuration as shown above. Be sure that `config.workdir` is set and the location exists in the local
113
+ machine.
114
+
115
+ To launch the packer, the only thing needed out of the box, is to instantiate 2 `S3DataPacker::Bucket`
116
+ objects that will act as source and destination:
117
+
118
+ ```
119
+ source_bucket = S3DataPacker::Sources::S3Bucket.new name: 'my-bucket', path: 'some/location'
120
+ target_bucket = S3DataPacker::Sources::S3Bucket.new name: 'other-bucket', path: 'my/destination'
121
+ ```
122
+
123
+ You can override the configured AWS credentials with the `:credentials` option, as well as `:region`.
124
+ `:credentials` needs to be an instance of `Aws::Credentials`. Having it setup this way should allow
125
+ for more complex role invoking, since the instance passed `:credentials` option is fed direclty to
126
+ `Aws::S3::Resource` and `Aws::S3::Client` to interface with the S3 buckets.
127
+
128
+ Once the buckets are instantiated you can call the packer:
129
+
130
+ ```
131
+ packer = S3DataPacker::Packer.new source: source_bucket, target: target_bucket
132
+ packer.pack!
133
+ ```
134
+
135
+ ### How it works?
136
+
137
+ Based on the sample above, what will happen once that `#pack!` is called, is that a set of threads will boot
138
+ up, a new file will be opened in `config.workdir` that, without further configuration it will be named
139
+ `123123123_batch.json` (in general), and then the packer will start to iterate over all keys under the
140
+ source path `some/location`.
141
+
142
+ Each key listed will enter the queue for the threads, and the threads will then take each key in queue,
143
+ download the data in memory (it does not create a file for it), append the data into the currently opened
144
+ batch, and continue with the next key.
145
+
146
+ As items are appended, if the target size `config.batch_size` is reached, the current batch is closed,
147
+ compressed with GZip, and uploaded to target bucket in the location specificed `my/destination`. Once
148
+ the file is pushed, the local copy is deleted, and a new batch is opened to continue appending items.
149
+
150
+ When all the keys have been listed, the packer will wait for the threads to finish any remaining items in the
151
+ queue, and the last opened batch that likely hasn't reached target size, is then closed and pushed like the
152
+ others.
153
+
154
+ And that's basically it. There are a few places in where additional processing may be introduced, but that's
155
+ a feature for later.
156
+
157
+ There are no specialties regarding source and target buckets, they can be the same, on different accounts
158
+ or region. However it is not recommended to setup source and target on the same bucket and path.
159
+
160
+ ### Custom Sources/Targets
161
+
162
+ It is possible to define a custom source and target for the packer to read data from some different place
163
+ that is not an S3 bucket, as well as put the resultant batch into somewhere else. The `S3DataPacker::Packer`
164
+ can take `:source` and `:target` parameters to use other things. At the moment, there are 2 source classes
165
+ provided:
166
+
167
+ - `S3DataPacker::Sources::S3Bucket`
168
+ - `S3DataPacker::Sources::Object`
169
+
170
+ And 2 pre-defined target:
171
+
172
+ - `S3DataPacker::Targets::S3Bucket`
173
+ - `S3DataPacker::Targets::Object`
174
+
175
+ Both bucket related classes operate in the same way, you need to define the name and path of the buckets
176
+ to read and write the data, as in the main example above. Be sure to configure credentials to use these.
177
+
178
+ The object source is pretty much a wrapper you can use with some other custom object, passing down which
179
+ methods to call on it for the packer. Any object you pass down in the object source needs to respond to:
180
+
181
+ - `#name`: which is mostly used for logging
182
+ - `#each`: with a block to iterate over items
183
+ - `#fetch`: with an identifier to find the actual data of the item
184
+
185
+ The `#each` and `#fetch` methods are like that mainly because the packer is threaded and it expects to
186
+ iterate over keys, or IDs or some minor piece of information in one thread, and use that information
187
+ to retrive the full object data on other threads. This keeps the queue small in byte size.
188
+
189
+ By default the object source expects those method names to be defined in the object provided. If there
190
+ are other methods that do that already on the object but with different name, the method names can be
191
+ passed like so:
192
+
193
+ ```ruby
194
+ S3DataPacker::Sources::Object.new object: my_object,
195
+ each_method: :iterate,
196
+ fetch_method: :find,
197
+ name_method: :display_name
198
+ ```
199
+
200
+ As long as `#each` yields items (strings, IDs, whatever), and `#fetch` returns JSON data for an item,
201
+ this should work.
202
+
203
+ For targets, there's also a `S3DataPacker::Targets::Object` that can be used in the a similar way, the only
204
+ 2 methods for it are:
205
+
206
+ - `#name`: for the same purposes as sources `#name` method
207
+ - `#save_file`: with a path parameter
208
+
209
+ It can also be configured with other method names if needed:
210
+
211
+ ```ruby
212
+ S3DataPacker::Targets::Object.new object: my_object,
213
+ name_method: :custom_name,
214
+ save_file_method: :save!
215
+ ```
216
+
217
+ It is also possible to construct a custom source/target class outside of the pre-defined ones that can
218
+ do anything needed, and passed down to the packer instance to use. As long as the few needed methods are
219
+ there, it should work just fine.
220
+
221
+ In some cases it might be useful to unify the get/fetch mechanics. This can be easily done by just
222
+ bypassing the `#fetch` method and returning the data received. If for some reason the iterator for
223
+ `#each` needs to output the actual data right there, by writing a `#fetch` method that returns whatever
224
+ was passed in the parameter, effectively makes the packer's queue hold actual data. This might be useful
225
+ in some cases, though it might need a smaller max size configuration to prevent having too much data in the
226
+ queue.
227
+
228
+ I believe that with these tools, the packer can pretty much do the JSON packing in most cases, including:
229
+
230
+ - Reading database records and serializing them into JSON
231
+ - Reading S3 buckets (as originally intended)
232
+ - Reading NoSQL items
233
+ - Reading one file or a set of files
234
+ - Writing batches into S3 buckets (as originally intended)
235
+ - Writing batches into filesystem on some custom location
236
+ - Writing batches into some other custom location
237
+
238
+ At least, it does cover the cases in where I intend to use it.
239
+
240
+ ## Development
241
+
242
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
243
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
244
+
245
+ ## Contributing
246
+
247
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rayko/s3_data_packer.
248
+ This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to
249
+ adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
250
+
251
+ ## License
252
+
253
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
254
+
255
+ ## Code of Conduct
256
+
257
+ Everyone interacting in the S3DataPacker project’s codebases, issue trackers, chat rooms and mailing lists
258
+ is expected to follow the [code of conduct](https://github.com/[USERNAME]/s3_data_packer/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "s3_data_packer"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,88 @@
1
+ module S3DataPacker
2
+ class Bucket
3
+ attr_reader :bucket_name, :path
4
+
5
+ def initialize opts = {}
6
+ @bucket_name = opts[:bucket_name]
7
+ @credentials = opts[:credentials]
8
+ @region = opts[:region]
9
+ @path = opts[:path]
10
+ end
11
+
12
+ def credentials
13
+ @credentials ||= S3DataPacker.config.default_s3_credentials
14
+ end
15
+
16
+ def region
17
+ @region ||= S3DataPacker.config.s3_region
18
+ end
19
+
20
+ def logger
21
+ @logger ||= S3DataPacker.logger
22
+ end
23
+
24
+ def each_key &block
25
+ bucket.objects(prefix: path).each do |item|
26
+ yield item.key
27
+ end
28
+ end
29
+
30
+ def exist?(key)
31
+ request! { object(key).exists? }
32
+ end
33
+
34
+ def download(key)
35
+ data = request! { object(key).get }
36
+ logger.warn "missing key #{key}" unless data
37
+ return nil unless data
38
+ data.body.read
39
+ end
40
+
41
+ def upload(file, opts={})
42
+ raise ArgumentError, 'File does not exist' unless File.exist?(file)
43
+ key = "#{path}/#{File.basename(file)}"
44
+ raise ArgumentError, "File #{File.basename(file)} already exists in target location" if exist?(key)
45
+ metadata = opts
46
+ metadata[:content_type] ||= file_mime_type(file)
47
+ metadata[:content_disposition] ||= 'attachement'
48
+ request! { object(key).upload_file(file, metadata) }
49
+ logger.info "Uploaded #{file} to s3://#{bucket_name}/#{key}"
50
+ end
51
+
52
+ private
53
+
54
+ def request! &block
55
+ begin
56
+ yield
57
+ rescue Aws::S3::Errors::InternalError
58
+ logger.warn "Aws::S3::Errors::InternalError, retrying in 1 second"
59
+ sleep(1)
60
+ retry
61
+ rescue Aws::S3::Errors::NoSuchKey
62
+ return nil
63
+ end
64
+ end
65
+
66
+ def file_mime_type(file)
67
+ begin
68
+ MIME::Types.type_for(file).first.content_type
69
+ rescue StandardError
70
+ logger.error "Could not guess MIME type of #{file}"
71
+ return nil
72
+ end
73
+ end
74
+
75
+ def object(key)
76
+ bucket.object(key)
77
+ end
78
+
79
+ def bucket
80
+ @bucket ||= resource.bucket(bucket_name)
81
+ end
82
+
83
+ def resource
84
+ @resource ||= ::Aws::S3::Resource.new(region: region, credentials: credentials)
85
+ end
86
+
87
+ end
88
+ end