webpacker_uploader 0.2.1 → 0.6.0

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