webpacker_uploader 0.2.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  [![Ruby tests](https://img.shields.io/github/workflow/status/tlatsas/webpacker_uploader/Ruby%20tests?style=flat-square)](https://github.com/tlatsas/webpacker_uploader/actions)
4
4
  [![RuboCop](https://img.shields.io/github/workflow/status/tlatsas/webpacker_uploader/RuboCop?label=rubocop&style=flat-square)](https://github.com/tlatsas/webpacker_uploader/actions)
5
5
  [![Gem](https://img.shields.io/gem/v/webpacker_uploader?style=flat-square)](https://rubygems.org/gems/webpacker_uploader)
6
+ [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg?style=flat-square)](https://www.rubydoc.info/gems/webpacker_uploader)
6
7
 
7
8
  Webpacker uploader provides an easy way to upload your assets to AWS S3.
8
9
  It knows which files to upload by reading the `manifest.json` file.
@@ -15,14 +16,14 @@ S3 + CDN, outside of your Rails application.
15
16
  Add this line to your application's Gemfile:
16
17
 
17
18
  ```ruby
18
- gem "webpacker_uploader"
19
+ gem "webpacker_uploader", require: false
19
20
  ```
20
21
 
21
- And then execute:
22
+ and run:
22
23
 
23
24
  $ bundle install
24
25
 
25
- Or install it yourself as:
26
+ or:
26
27
 
27
28
  $ gem install webpacker_uploader
28
29
 
@@ -34,6 +35,8 @@ gem "aws-sdk-s3", require: false
34
35
 
35
36
  ## Usage
36
37
 
38
+ Usually, in a Rake task you would add the following to upload the assets to AWS S3:
39
+
37
40
  ```ruby
38
41
  require "webpacker_uploader"
39
42
  require "webpacker_uploader/providers/aws"
@@ -89,18 +92,6 @@ provider_options = {
89
92
  provider = WebpackerUploader::Providers::Aws.new(provider_options)
90
93
  ```
91
94
 
92
- ### Ignore files
93
-
94
- The uploader can be configured to skip certain files based on the file extension.
95
- By default `.map` files are excluded. This can be configured through the `ignored_extensions` attribute.
96
- In order to upload everything pass an empty array.
97
-
98
- ```ruby
99
- # skip uploading images
100
- WebpackerUploader.ignored_extensions = [".png", ".jpg", ".webp"]
101
- WebpackerUploader.upload!(provider)
102
- ```
103
-
104
95
  ### Prefix remote files
105
96
 
106
97
  Uploaded files can be prefixed by setting the `prefix` parameter during upload:
@@ -112,6 +103,43 @@ WebpackerUploader.upload!(provider, prefix: "assets")
112
103
  This will prefix all remote file paths with `assets` so instead of storing `packs/application-dd6b1cd38bfa093df600.css` it
113
104
  will store `assets/packs/application-dd6b1cd38bfa093df600.css`.
114
105
 
106
+ ### Configuration
107
+
108
+ WebpackerUploader currently supports the following configuration options:
109
+
110
+ | option | description | default value |
111
+ |----------------------|--------------------------------------------------------------|---------------------------------------|
112
+ | ignored_extensions | Which files uploader should skip based on the file extension | [] |
113
+ | logger | The logger to use for logging | ActiveSupport::Logger.new(STDOUT) |
114
+ | log_output | Log output as we upload files | true |
115
+ | public_manifest_path | The webpack manifest path | Webpacker.config.public_manifest_path |
116
+ | public_path | The webpack public output path | Webpacker.config.public_path |
117
+
118
+ It can be configured using a block:
119
+
120
+ ```ruby
121
+ WebpackerUploader.configure do |config|
122
+ config.ignored_extensions = [".png", ".jpg", ".webp"]
123
+ config.log_output = false
124
+ config.public_manifest_path = "path/to/manifest.json"
125
+ config.public_path = "path/to/public/dir"
126
+ end
127
+ ```
128
+
129
+ or directly:
130
+
131
+ ```ruby
132
+ WebpackerUploader.config.log_output = false
133
+ ```
134
+
135
+ The uploader used to skip `.map` files by default. This has changed (see [CHANGELOG.md](https://github.com/tlatsas/webpacker_uploader/blob/main/CHANGELOG.md))
136
+ and everything is uploaded by default now. To retain the previous functionality use:
137
+
138
+ ```ruby
139
+ # skip uploading map files
140
+ WebpackerUploader.config.ignored_extensions = [".map"]
141
+ ```
142
+
115
143
  ## Development
116
144
 
117
145
  After checking out the repo, run `bin/setup` to install dependencies.
@@ -120,6 +148,19 @@ interactive prompt that will allow you to experiment.
120
148
 
121
149
  To install this gem onto your local machine, run `bundle exec rake install`.
122
150
 
151
+ ## Integration tests
152
+
153
+ This gem also provides an integration test suite. It runs using [localstack](https://github.com/localstack/localstack/).
154
+ To run the integration tests, install docker on your system and spin up a localstack container running the s3 service.
155
+
156
+ ```shell
157
+ docker compose -f "integration/docker-compose.yml" up --detach
158
+ ```
159
+
160
+ To run the tests, use `rake test:integration`.
161
+
162
+ To stop the container once done run: `docker-compose -f "integration/docker-compose.yml" down`
163
+
123
164
  ## Contributing
124
165
 
125
166
  Bug reports and pull requests are welcome on GitHub at https://github.com/tlatsas/webpacker_uploader.
data/Rakefile CHANGED
@@ -1,10 +1,23 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require "rake/testtask"
3
5
 
6
+ require "yard"
7
+ YARD::Rake::YardocTask.new
8
+
4
9
  Rake::TestTask.new(:test) do |t|
5
10
  t.libs << "test"
6
11
  t.libs << "lib"
7
12
  t.test_files = FileList["test/**/*_test.rb"]
8
13
  end
9
14
 
15
+ namespace :test do
16
+ desc "Run integration tests using localstack"
17
+ Rake::TestTask.new(:integration) do |t|
18
+ t.libs << "integration"
19
+ t.test_files = FileList["integration/**/*_test.rb"]
20
+ end
21
+ end
22
+
10
23
  task default: :test
@@ -1,23 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/module"
3
4
  require "active_support/core_ext/object/blank"
4
- require "active_support/logger"
5
- require "active_support/tagged_logging"
6
5
 
7
6
  module WebpackerUploader
8
7
  extend self
9
8
 
9
+ # @private
10
10
  def instance=(instance)
11
11
  @instance = instance
12
12
  end
13
13
 
14
+ # @private
14
15
  def instance
15
16
  @instance ||= WebpackerUploader::Instance.new
16
17
  end
17
18
 
18
- delegate :logger, :logger=, :upload!, :ignored_extensions, :ignored_extensions=, to: :instance
19
+ # @!attribute [rw] config
20
+ # @see Instance#config
21
+ # @!scope class
22
+ # @!method configure
23
+ # @see Instance#configure
24
+ # @!scope class
25
+ # @!method upload!
26
+ # @return [void]
27
+ # @see Instance#upload!
28
+ # @!scope class
29
+ delegate :configure, :config, :upload!, to: :instance
19
30
  end
20
31
 
32
+ require "webpacker_uploader/configuration"
21
33
  require "webpacker_uploader/instance"
22
34
  require "webpacker_uploader/manifest"
23
35
  require "webpacker_uploader/mime"
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/logger"
4
+ require "active_support/tagged_logging"
5
+
6
+ # This is the class which holds the configuration options.
7
+ #
8
+ # Options are set and retrieved using `WebpackerUploader.config`
9
+ # and `WebpackerUploader.configure`.
10
+ class WebpackerUploader::Configuration
11
+ # @return [Array] the file extentions ignored by the uploader.
12
+ attr_accessor :ignored_extensions
13
+
14
+ # @return [ActiveSupport::Logger] the logger to use.
15
+ attr_accessor :logger
16
+
17
+ # @return [Boolean] whether or not to log operations.
18
+ attr_accessor :log_output
19
+
20
+ # @return [Pathname] the path to manifest.json, defaults to Webpacker public manifest path.
21
+ attr_reader :public_manifest_path
22
+
23
+ # @return [Pathname] the public root path, defaults to Webpacker public root path.
24
+ attr_reader :public_path
25
+
26
+ alias_method :log_output?, :log_output
27
+
28
+ def initialize
29
+ @ignored_extensions = []
30
+ @logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
31
+ @log_output = true
32
+ @public_manifest_path = ::Webpacker.config.public_manifest_path
33
+ @public_path = ::Webpacker.config.public_path
34
+ end
35
+
36
+ def public_manifest_path=(path)
37
+ @public_manifest_path = Pathname.new(path)
38
+ end
39
+
40
+ def public_path=(path)
41
+ @public_path = Pathname.new(path)
42
+ end
43
+ end
@@ -1,18 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class WebpackerUploader::Instance
4
- cattr_accessor(:logger) { ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT)) }
5
- cattr_accessor(:ignored_extensions) { %w[.map] }
4
+ attr_writer :config
6
5
 
6
+ # @private
7
7
  def manifest
8
8
  @manifest ||= WebpackerUploader::Manifest.new
9
9
  end
10
10
 
11
- def upload!(provider, prefix: nil)
12
- if ignored_extensions.nil?
13
- raise ArgumentError, "Ignored extensions should be specified as an array"
14
- end
11
+ # Returns the global WebpackerUploader::Configuration object.
12
+ # Use this to set and retrieve specific configuration options.
13
+ #
14
+ # @return [WebpackerUploader::Configuration]
15
+ #
16
+ # @example Disable log output
17
+ # WebpackerUploader.config.log_output = false
18
+ #
19
+ # @example Get the list of excluded file extension
20
+ # puts WebpackerUploader.config.ignored_extensions
21
+ #
22
+ # @see WebpackerUploader::Configuration
23
+ def config
24
+ @config ||= WebpackerUploader::Configuration.new
25
+ end
15
26
 
27
+ # Yields the global configuration to a block. Use this to
28
+ # configure the base gem features.
29
+ #
30
+ # @example
31
+ # WebpackerUploader.configure do |config|
32
+ # config.ignored_extensions = [".png", ".jpg", ".webp"]
33
+ # config.log_output = false
34
+ # config.public_manifest_path = "path/to/manifest.json"
35
+ # config.public_path = "path/to/public/dir"
36
+ # end
37
+ #
38
+ # @see WebpackerUploader::Configuration
39
+ def configure
40
+ yield config
41
+ end
42
+
43
+ # Uploads assets using the supplied provider. Currently only AWS S3 is implemented.
44
+ #
45
+ # @return [void]
46
+ # @param provider [WebpackerUploader::Providers::Aws] A provider to use for file uploading.
47
+ # @param prefix [String] Used to prefix the remote file paths.
48
+ def upload!(provider, prefix: nil)
16
49
  manifest.assets.each do |name, js_path|
17
50
  path = js_path[1..-1]
18
51
 
@@ -23,13 +56,14 @@ class WebpackerUploader::Instance
23
56
  "#{prefix}/#{path}"
24
57
  end
25
58
 
26
- file_path = Rails.root.join("public", path)
59
+ file_path = config.public_path.join(path)
27
60
 
28
- if name.end_with?(*ignored_extensions)
29
- logger.info("Skipping #{file_path}")
61
+ if name.end_with?(*config.ignored_extensions)
62
+ config.logger.info("Skipping #{file_path}") if config.log_output?
30
63
  else
31
64
  content_type = WebpackerUploader::Mime.mime_type(path)
32
- logger.info("Processing #{file_path} as #{content_type}")
65
+
66
+ config.logger.info("Processing #{file_path} as #{content_type}") if config.log_output?
33
67
 
34
68
  provider.upload!(remote_path, file_path, content_type)
35
69
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class WebpackerUploader::Manifest
3
+ class WebpackerUploader::Manifest # @private
4
4
  attr_reader :assets
5
5
 
6
6
  def initialize
@@ -10,8 +10,8 @@ class WebpackerUploader::Manifest
10
10
  private
11
11
 
12
12
  def load
13
- if ::Webpacker.config.public_manifest_path.exist?
14
- JSON.parse(::Webpacker.config.public_manifest_path.read).except("entrypoints")
13
+ if WebpackerUploader.config.public_manifest_path.exist?
14
+ JSON.parse(WebpackerUploader.config.public_manifest_path.read).except("entrypoints")
15
15
  else
16
16
  {}
17
17
  end
@@ -3,6 +3,12 @@
3
3
  require "mime-types"
4
4
 
5
5
  module WebpackerUploader::Mime
6
+ # Returns the mime type for the given file in the filesystem.
7
+ # If it's unable to detect the mime type, it returns +application/octet-stream+
8
+ # as a fallback.
9
+ #
10
+ # @param file_path [String] A file path in the local filesystem.
11
+ # @return [String] The file mime type.
6
12
  def mime_type(file_path)
7
13
  fallback = MIME::Types.type_for(file_path).first&.content_type || "application/octet-stream"
8
14
  Rack::Mime.mime_type(File.extname(file_path), fallback)
@@ -3,34 +3,105 @@
3
3
  require "aws-sdk-s3"
4
4
 
5
5
  module WebpackerUploader
6
+ # Namespace for the upload providers.
6
7
  module Providers
8
+ # AWS provider uploads files to AWS S3. It uses the +aws-sdk-s3+ gem.
7
9
  class Aws
8
- attr_reader :client
9
- attr_reader :resource
10
- attr_reader :bucket
10
+ # AWS provider error class. Raised when the provided AWS client credentials options are wrong.
11
+ class CredentialsError < StandardError; end
11
12
 
13
+ # @param [Hash] options
14
+ # * :region (String) The S3 region name.
15
+ # * :bucket (String) The S3 bucket name.
16
+ # * :credentials (Hash) credential options for the AWS provider:
17
+ # * :profile_name (String) use a named profile configured in ~/.aws/credentials
18
+ # * :instance_profile (Boolean) use an instance profile from an EC2
19
+ # * :access_key_id (String) the AWS credentials access id.
20
+ # * :secret_access_key (String) the AWS credentials secret access key.
21
+ #
22
+ # @note Any unknown options will be passed directly to the +Aws::S3::Client+ class
23
+ # during initialization.
24
+ #
25
+ # @raise [CredentialsError] if the credential options Hash is not correct.
26
+ #
27
+ # @example Initialize using a named profile:
28
+ #
29
+ # provider_options = {
30
+ # credentials: { profile_name: "staging" },
31
+ # region: "eu-central-1",
32
+ # bucket: "application-assets-20200929124523451600000001"
33
+ # }
34
+ # provider = WebpackerUploader::Providers::Aws.new(provider_options)
35
+ #
36
+ # @example Initialize using IAM keys
37
+ #
38
+ # provider_options = {
39
+ # credentials: { access_key_id: "KEY_ID", secret_access_key: "ACCESS_KEY" },
40
+ # region: "eu-central-1",
41
+ # bucket: "application-assets-20200929124523451600000001"
42
+ # }
43
+ # provider = WebpackerUploader::Providers::Aws.new(provider_options)
44
+ #
45
+ # @example Initialize using an EC2 instance profile
46
+ #
47
+ # provider_options = {
48
+ # credentials: { instance_profile: true },
49
+ # region: "eu-central-1",
50
+ # bucket: "application-assets-20200929124523451600000001"
51
+ # }
52
+ # provider = WebpackerUploader::Providers::Aws.new(provider_options)
12
53
  def initialize(options)
13
- @client = ::Aws::S3::Client.new(credentials: credentials(options[:credentials]), region: options[:region])
14
- @resource = ::Aws::S3::Resource.new(client: @client)
15
- @bucket = @resource.bucket(options[:bucket])
54
+ @region = options.delete(:region)
55
+ @bucket_name = options.delete(:bucket)
56
+ @credentials = credentials(options.delete(:credentials))
57
+ @aws_options = options
58
+ @resource = ::Aws::S3::Resource.new(client: client)
16
59
  end
17
60
 
61
+ # Uploads a file to AWS S3.
62
+ #
63
+ # @param object_key [String] Is the remote path name for the S3 object.
64
+ # @param file [Pathname] Path of the local file.
65
+ # @param content_type [String] The content type that will be set to the S3 object.
66
+ # @return [void]
18
67
  def upload!(object_key, file, content_type = "")
19
- object = @bucket.object(object_key)
68
+ object = @resource.bucket(@bucket_name).object(object_key)
20
69
  object.upload_file(file, content_type: content_type)
21
70
  end
22
71
 
23
72
  private
24
-
25
73
  def credentials(options)
26
- if options[:profile_name].present?
27
- ::Aws::SharedCredentials.new(profile_name: options[:profile_name])
74
+ if options.key?(:profile_name)
75
+ { profile: options[:profile_name] }
28
76
  elsif options.key?(:instance_profile) && options[:instance_profile]
29
77
  ::Aws::InstanceProfileCredentials.new
30
- else
78
+ elsif options.key?(:access_key_id) && options.key?(:secret_access_key)
31
79
  ::Aws::Credentials.new(options[:access_key_id], options[:secret_access_key])
80
+ else
81
+ raise CredentialsError, "Wrong AWS provider credentials options."
32
82
  end
33
83
  end
84
+
85
+ def profile?
86
+ @credentials.is_a?(Hash) && @credentials.key?(:profile)
87
+ end
88
+
89
+ def credentials_object?
90
+ !profile?
91
+ end
92
+
93
+ def client
94
+ ::Aws::S3::Client.new(client_options)
95
+ end
96
+
97
+ def client_options
98
+ opts = {}
99
+ opts.merge!(@aws_options)
100
+ opts[:region] = @region
101
+ opts.merge!(@credentials) if profile?
102
+ opts[:credentials] = @credentials if credentials_object?
103
+ opts
104
+ end
34
105
  end
35
106
  end
36
107
  end