aboutsummaryrefslogtreecommitdiffstats
path: root/activestorage
diff options
context:
space:
mode:
Diffstat (limited to 'activestorage')
-rw-r--r--activestorage/.babelrc5
-rw-r--r--activestorage/.eslintrc19
-rw-r--r--activestorage/.gitignore6
-rw-r--r--activestorage/CHANGELOG.md12
-rw-r--r--activestorage/MIT-LICENSE20
-rw-r--r--activestorage/README.md159
-rw-r--r--activestorage/Rakefile16
-rw-r--r--activestorage/activestorage.gemspec30
-rw-r--r--activestorage/app/assets/javascripts/activestorage.js1
-rw-r--r--activestorage/app/controllers/active_storage/blobs_controller.rb14
-rw-r--r--activestorage/app/controllers/active_storage/direct_uploads_controller.rb23
-rw-r--r--activestorage/app/controllers/active_storage/disk_controller.rb51
-rw-r--r--activestorage/app/controllers/active_storage/previews_controller.rb10
-rw-r--r--activestorage/app/controllers/active_storage/variants_controller.rb14
-rw-r--r--activestorage/app/controllers/concerns/active_storage/set_blob.rb16
-rw-r--r--activestorage/app/javascript/activestorage/blob_record.js68
-rw-r--r--activestorage/app/javascript/activestorage/blob_upload.js35
-rw-r--r--activestorage/app/javascript/activestorage/direct_upload.js42
-rw-r--r--activestorage/app/javascript/activestorage/direct_upload_controller.js67
-rw-r--r--activestorage/app/javascript/activestorage/direct_uploads_controller.js50
-rw-r--r--activestorage/app/javascript/activestorage/file_checksum.js53
-rw-r--r--activestorage/app/javascript/activestorage/helpers.js42
-rw-r--r--activestorage/app/javascript/activestorage/index.js11
-rw-r--r--activestorage/app/javascript/activestorage/ujs.js75
-rw-r--r--activestorage/app/jobs/active_storage/analyze_job.rb8
-rw-r--r--activestorage/app/jobs/active_storage/base_job.rb5
-rw-r--r--activestorage/app/jobs/active_storage/purge_job.rb11
-rw-r--r--activestorage/app/models/active_storage/attachment.rb35
-rw-r--r--activestorage/app/models/active_storage/blob.rb328
-rw-r--r--activestorage/app/models/active_storage/filename.rb73
-rw-r--r--activestorage/app/models/active_storage/filename/parameters.rb36
-rw-r--r--activestorage/app/models/active_storage/preview.rb90
-rw-r--r--activestorage/app/models/active_storage/variant.rb132
-rw-r--r--activestorage/app/models/active_storage/variation.rb79
-rwxr-xr-xactivestorage/bin/test5
-rw-r--r--activestorage/config/routes.rb43
-rw-r--r--activestorage/db/migrate/20170806125915_create_active_storage_tables.rb25
-rw-r--r--activestorage/lib/active_storage.rb45
-rw-r--r--activestorage/lib/active_storage/analyzer.rb33
-rw-r--r--activestorage/lib/active_storage/analyzer/image_analyzer.rb36
-rw-r--r--activestorage/lib/active_storage/analyzer/null_analyzer.rb13
-rw-r--r--activestorage/lib/active_storage/analyzer/video_analyzer.rb93
-rw-r--r--activestorage/lib/active_storage/attached.rb40
-rw-r--r--activestorage/lib/active_storage/attached/macros.rb96
-rw-r--r--activestorage/lib/active_storage/attached/many.rb63
-rw-r--r--activestorage/lib/active_storage/attached/one.rb83
-rw-r--r--activestorage/lib/active_storage/downloading.rb26
-rw-r--r--activestorage/lib/active_storage/engine.rb91
-rw-r--r--activestorage/lib/active_storage/gem_version.rb17
-rw-r--r--activestorage/lib/active_storage/log_subscriber.rb56
-rw-r--r--activestorage/lib/active_storage/previewer.rb62
-rw-r--r--activestorage/lib/active_storage/previewer/pdf_previewer.rb24
-rw-r--r--activestorage/lib/active_storage/previewer/video_previewer.rb25
-rw-r--r--activestorage/lib/active_storage/service.rb127
-rw-r--r--activestorage/lib/active_storage/service/azure_storage_service.rb140
-rw-r--r--activestorage/lib/active_storage/service/configurator.rb32
-rw-r--r--activestorage/lib/active_storage/service/disk_service.rb137
-rw-r--r--activestorage/lib/active_storage/service/gcs_service.rb113
-rw-r--r--activestorage/lib/active_storage/service/mirror_service.rb55
-rw-r--r--activestorage/lib/active_storage/service/s3_service.rb106
-rw-r--r--activestorage/lib/active_storage/version.rb10
-rw-r--r--activestorage/lib/tasks/activestorage.rake12
-rw-r--r--activestorage/package.json33
-rw-r--r--activestorage/test/analyzer/image_analyzer_test.rb24
-rw-r--r--activestorage/test/analyzer/video_analyzer_test.rb35
-rw-r--r--activestorage/test/controllers/blobs_controller_test.rb22
-rw-r--r--activestorage/test/controllers/direct_uploads_controller_test.rb124
-rw-r--r--activestorage/test/controllers/disk_controller_test.rb59
-rw-r--r--activestorage/test/controllers/previews_controller_test.rb33
-rw-r--r--activestorage/test/controllers/variants_controller_test.rb32
-rw-r--r--activestorage/test/database/create_users_migration.rb9
-rw-r--r--activestorage/test/database/setup.rb7
-rw-r--r--activestorage/test/dummy/Rakefile5
-rw-r--r--activestorage/test/dummy/app/assets/config/manifest.js5
-rw-r--r--activestorage/test/dummy/app/assets/images/.keep0
-rw-r--r--activestorage/test/dummy/app/assets/javascripts/application.js13
-rw-r--r--activestorage/test/dummy/app/assets/stylesheets/application.css15
-rw-r--r--activestorage/test/dummy/app/controllers/application_controller.rb5
-rw-r--r--activestorage/test/dummy/app/controllers/concerns/.keep0
-rw-r--r--activestorage/test/dummy/app/helpers/application_helper.rb4
-rw-r--r--activestorage/test/dummy/app/jobs/application_job.rb4
-rw-r--r--activestorage/test/dummy/app/models/application_record.rb5
-rw-r--r--activestorage/test/dummy/app/models/concerns/.keep0
-rw-r--r--activestorage/test/dummy/app/views/layouts/application.html.erb14
-rwxr-xr-xactivestorage/test/dummy/bin/bundle5
-rwxr-xr-xactivestorage/test/dummy/bin/rails6
-rwxr-xr-xactivestorage/test/dummy/bin/rake6
-rwxr-xr-xactivestorage/test/dummy/bin/yarn13
-rw-r--r--activestorage/test/dummy/config.ru7
-rw-r--r--activestorage/test/dummy/config/application.rb22
-rw-r--r--activestorage/test/dummy/config/boot.rb11
-rw-r--r--activestorage/test/dummy/config/database.yml25
-rw-r--r--activestorage/test/dummy/config/environment.rb7
-rw-r--r--activestorage/test/dummy/config/environments/development.rb51
-rw-r--r--activestorage/test/dummy/config/environments/production.rb84
-rw-r--r--activestorage/test/dummy/config/environments/test.rb38
-rw-r--r--activestorage/test/dummy/config/initializers/application_controller_renderer.rb7
-rw-r--r--activestorage/test/dummy/config/initializers/assets.rb16
-rw-r--r--activestorage/test/dummy/config/initializers/backtrace_silencers.rb8
-rw-r--r--activestorage/test/dummy/config/initializers/cookies_serializer.rb7
-rw-r--r--activestorage/test/dummy/config/initializers/filter_parameter_logging.rb6
-rw-r--r--activestorage/test/dummy/config/initializers/inflections.rb17
-rw-r--r--activestorage/test/dummy/config/initializers/mime_types.rb5
-rw-r--r--activestorage/test/dummy/config/initializers/wrap_parameters.rb16
-rw-r--r--activestorage/test/dummy/config/routes.rb4
-rw-r--r--activestorage/test/dummy/config/secrets.yml32
-rw-r--r--activestorage/test/dummy/config/spring.rb8
-rw-r--r--activestorage/test/dummy/config/storage.yml3
-rw-r--r--activestorage/test/dummy/lib/assets/.keep0
-rw-r--r--activestorage/test/dummy/log/.keep0
-rw-r--r--activestorage/test/dummy/package.json5
-rw-r--r--activestorage/test/dummy/public/404.html67
-rw-r--r--activestorage/test/dummy/public/422.html67
-rw-r--r--activestorage/test/dummy/public/500.html66
-rw-r--r--activestorage/test/dummy/public/apple-touch-icon-precomposed.png0
-rw-r--r--activestorage/test/dummy/public/apple-touch-icon.png0
-rw-r--r--activestorage/test/dummy/public/favicon.ico0
-rw-r--r--activestorage/test/fixtures/files/icon.psdbin0 -> 37441 bytes
-rw-r--r--activestorage/test/fixtures/files/icon.svg13
-rw-r--r--activestorage/test/fixtures/files/image.gifbin0 -> 2032 bytes
-rw-r--r--activestorage/test/fixtures/files/racecar.jpgbin0 -> 1124062 bytes
-rw-r--r--activestorage/test/fixtures/files/report.pdfbin0 -> 13469 bytes
-rw-r--r--activestorage/test/fixtures/files/rotated_video.mp4bin0 -> 275090 bytes
-rw-r--r--activestorage/test/fixtures/files/video.mp4bin0 -> 275433 bytes
-rw-r--r--activestorage/test/fixtures/files/video_without_video_stream.mp4bin0 -> 16252 bytes
-rw-r--r--activestorage/test/models/attachments_test.rb347
-rw-r--r--activestorage/test/models/blob_test.rb64
-rw-r--r--activestorage/test/models/filename/parameters_test.rb32
-rw-r--r--activestorage/test/models/filename_test.rb56
-rw-r--r--activestorage/test/models/preview_test.rb40
-rw-r--r--activestorage/test/models/representation_test.rb41
-rw-r--r--activestorage/test/models/variant_test.rb72
-rw-r--r--activestorage/test/previewer/pdf_previewer_test.rb23
-rw-r--r--activestorage/test/previewer/video_previewer_test.rb23
-rw-r--r--activestorage/test/service/azure_storage_service_test.rb22
-rw-r--r--activestorage/test/service/configurations.example.yml30
-rw-r--r--activestorage/test/service/configurations.yml.encbin0 -> 2864 bytes
-rw-r--r--activestorage/test/service/configurator_test.rb16
-rw-r--r--activestorage/test/service/disk_service_test.rb14
-rw-r--r--activestorage/test/service/gcs_service_test.rb55
-rw-r--r--activestorage/test/service/mirror_service_test.rb66
-rw-r--r--activestorage/test/service/s3_service_test.rb59
-rw-r--r--activestorage/test/service/shared_service_tests.rb96
-rw-r--r--activestorage/test/template/image_tag_test.rb44
-rw-r--r--activestorage/test/test_helper.rb69
-rw-r--r--activestorage/webpack.config.js26
-rw-r--r--activestorage/yarn.lock3154
147 files changed, 8663 insertions, 0 deletions
diff --git a/activestorage/.babelrc b/activestorage/.babelrc
new file mode 100644
index 0000000000..a8211d329f
--- /dev/null
+++ b/activestorage/.babelrc
@@ -0,0 +1,5 @@
+{
+ "presets": [
+ ["env", { "modules": false } ]
+ ]
+}
diff --git a/activestorage/.eslintrc b/activestorage/.eslintrc
new file mode 100644
index 0000000000..3d9ecd4bce
--- /dev/null
+++ b/activestorage/.eslintrc
@@ -0,0 +1,19 @@
+{
+ "extends": "eslint:recommended",
+ "rules": {
+ "semi": ["error", "never"],
+ "quotes": ["error", "double"],
+ "no-unused-vars": ["error", { "vars": "all", "args": "none" }]
+ },
+ "plugins": [
+ "import"
+ ],
+ "env": {
+ "browser": true,
+ "es6": true
+ },
+ "parserOptions": {
+ "ecmaVersion": 6,
+ "sourceType": "module"
+ }
+}
diff --git a/activestorage/.gitignore b/activestorage/.gitignore
new file mode 100644
index 0000000000..a532335bdd
--- /dev/null
+++ b/activestorage/.gitignore
@@ -0,0 +1,6 @@
+.byebug_history
+node_modules
+test/dummy/db/*.sqlite3
+test/dummy/db/*.sqlite3-journal
+test/dummy/log/*.log
+test/dummy/tmp/
diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md
new file mode 100644
index 0000000000..c5171e7490
--- /dev/null
+++ b/activestorage/CHANGELOG.md
@@ -0,0 +1,12 @@
+## Rails 5.2.0.beta2 (November 28, 2017) ##
+
+* Fix the gem adding the migrations files to the package.
+
+ *Yuji Yaginuma*
+
+
+## Rails 5.2.0.beta1 (November 27, 2017) ##
+
+* Added to Rails.
+
+ *DHH*
diff --git a/activestorage/MIT-LICENSE b/activestorage/MIT-LICENSE
new file mode 100644
index 0000000000..eed89ac398
--- /dev/null
+++ b/activestorage/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2017-2018 David Heinemeier Hansson, Basecamp
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/activestorage/README.md b/activestorage/README.md
new file mode 100644
index 0000000000..85ab70dac6
--- /dev/null
+++ b/activestorage/README.md
@@ -0,0 +1,159 @@
+# Active Storage
+
+Active Storage makes it simple to upload and reference files in cloud services like [Amazon S3](https://aws.amazon.com/s3/), [Google Cloud Storage](https://cloud.google.com/storage/docs/), or [Microsoft Azure Storage](https://azure.microsoft.com/en-us/services/storage/), and attach those files to Active Records. Supports having one main service and mirrors in other services for redundancy. It also provides a disk service for testing or local deployments, but the focus is on cloud storage.
+
+Files can be uploaded from the server to the cloud or directly from the client to the cloud.
+
+Image files can furthermore be transformed using on-demand variants for quality, aspect ratio, size, or any other [MiniMagick](https://github.com/minimagick/minimagick) supported transformation.
+
+## Compared to other storage solutions
+
+A key difference to how Active Storage works compared to other attachment solutions in Rails is through the use of built-in [Blob](https://github.com/rails/rails/blob/master/activestorage/app/models/active_storage/blob.rb) and [Attachment](https://github.com/rails/rails/blob/master/activestorage/app/models/active_storage/attachment.rb) models (backed by Active Record). This means existing application models do not need to be modified with additional columns to associate with files. Active Storage uses polymorphic associations via the `Attachment` join model, which then connects to the actual `Blob`.
+
+`Blob` models store attachment metadata (filename, content-type, etc.), and their identifier key in the storage service. Blob models do not store the actual binary data. They are intended to be immutable in spirit. One file, one blob. You can associate the same blob with multiple application models as well. And if you want to do transformations of a given `Blob`, the idea is that you'll simply create a new one, rather than attempt to mutate the existing one (though of course you can delete the previous version later if you don't need it).
+
+## Installation
+
+Run `rails active_storage:install` to copy over active_storage migrations.
+
+## Examples
+
+One attachment:
+
+```ruby
+class User < ApplicationRecord
+ # Associates an attachment and a blob. When the user is destroyed they are
+ # purged by default (models destroyed, and resource files deleted).
+ has_one_attached :avatar
+end
+
+# Attach an avatar to the user.
+user.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg")
+
+# Does the user have an avatar?
+user.avatar.attached? # => true
+
+# Synchronously destroy the avatar and actual resource files.
+user.avatar.purge
+
+# Destroy the associated models and actual resource files async, via Active Job.
+user.avatar.purge_later
+
+# Does the user have an avatar?
+user.avatar.attached? # => false
+
+# Generate a permanent URL for the blob that points to the application.
+# Upon access, a redirect to the actual service endpoint is returned.
+# This indirection decouples the public URL from the actual one, and
+# allows for example mirroring attachments in different services for
+# high-availability. The redirection has an HTTP expiration of 5 min.
+url_for(user.avatar)
+
+class AvatarsController < ApplicationController
+ def update
+ # params[:avatar] contains a ActionDispatch::Http::UploadedFile object
+ Current.user.avatar.attach(params.require(:avatar))
+ redirect_to Current.user
+ end
+end
+```
+
+Many attachments:
+
+```ruby
+class Message < ApplicationRecord
+ has_many_attached :images
+end
+```
+
+```erb
+<%= form_with model: @message, local: true do |form| %>
+ <%= form.text_field :title, placeholder: "Title" %><br>
+ <%= form.text_area :content %><br><br>
+
+ <%= form.file_field :images, multiple: true %><br>
+ <%= form.submit %>
+<% end %>
+```
+
+```ruby
+class MessagesController < ApplicationController
+ def index
+ # Use the built-in with_attached_images scope to avoid N+1
+ @messages = Message.all.with_attached_images
+ end
+
+ def create
+ message = Message.create! params.require(:message).permit(:title, :content)
+ message.images.attach(params[:message][:images])
+ redirect_to message
+ end
+
+ def show
+ @message = Message.find(params[:id])
+ end
+end
+```
+
+Variation of image attachment:
+
+```erb
+<%# Hitting the variant URL will lazy transform the original blob and then redirect to its new service location %>
+<%= image_tag user.avatar.variant(resize: "100x100") %>
+```
+
+## Direct uploads
+
+Active Storage, with its included JavaScript library, supports uploading directly from the client to the cloud.
+
+### Direct upload installation
+
+1. Include `activestorage.js` in your application's JavaScript bundle.
+
+ Using the asset pipeline:
+ ```js
+ //= require activestorage
+ ```
+ Using the npm package:
+ ```js
+ import * as ActiveStorage from "activestorage"
+ ActiveStorage.start()
+ ```
+2. Annotate file inputs with the direct upload URL.
+
+ ```ruby
+ <%= form.file_field :attachments, multiple: true, direct_upload: true %>
+ ```
+3. That's it! Uploads begin upon form submission.
+
+### Direct upload JavaScript events
+
+| Event name | Event target | Event data (`event.detail`) | Description |
+| --- | --- | --- | --- |
+| `direct-uploads:start` | `<form>` | None | A form containing files for direct upload fields was submitted. |
+| `direct-upload:initialize` | `<input>` | `{id, file}` | Dispatched for every file after form submission. |
+| `direct-upload:start` | `<input>` | `{id, file}` | A direct upload is starting. |
+| `direct-upload:before-blob-request` | `<input>` | `{id, file, xhr}` | Before making a request to your application for direct upload metadata. |
+| `direct-upload:before-storage-request` | `<input>` | `{id, file, xhr}` | Before making a request to store a file. |
+| `direct-upload:progress` | `<input>` | `{id, file, progress}` | As requests to store files progress. |
+| `direct-upload:error` | `<input>` | `{id, file, error}` | An error occurred. An `alert` will display unless this event is canceled. |
+| `direct-upload:end` | `<input>` | `{id, file}` | A direct upload has ended. |
+| `direct-uploads:end` | `<form>` | None | All direct uploads have ended. |
+
+## License
+
+Active Storage is released under the [MIT License](https://opensource.org/licenses/MIT).
+
+## Support
+
+API documentation is at:
+
+* http://api.rubyonrails.org
+
+Bug reports for the Ruby on Rails project can be filed here:
+
+* https://github.com/rails/rails/issues
+
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
diff --git a/activestorage/Rakefile b/activestorage/Rakefile
new file mode 100644
index 0000000000..2aa4d2a76f
--- /dev/null
+++ b/activestorage/Rakefile
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require "bundler/setup"
+require "bundler/gem_tasks"
+require "rake/testtask"
+
+Rake::TestTask.new do |test|
+ test.libs << "app/controllers"
+ test.libs << "test"
+ test.test_files = FileList["test/**/*_test.rb"]
+ test.warning = false
+end
+
+task :package
+
+task default: :test
diff --git a/activestorage/activestorage.gemspec b/activestorage/activestorage.gemspec
new file mode 100644
index 0000000000..7f7f1a26ac
--- /dev/null
+++ b/activestorage/activestorage.gemspec
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip
+
+Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = "activestorage"
+ s.version = version
+ s.summary = "Local and cloud file storage framework."
+ s.description = "Attach cloud and local files in Rails applications."
+
+ s.required_ruby_version = ">= 2.2.2"
+
+ s.license = "MIT"
+
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "http://rubyonrails.org"
+
+ s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/**/*", "config/**/*", "db/**/*"]
+ s.require_path = "lib"
+
+ s.metadata = {
+ "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/activestorage",
+ "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activestorage/CHANGELOG.md"
+ }
+
+ s.add_dependency "actionpack", version
+ s.add_dependency "activerecord", version
+end
diff --git a/activestorage/app/assets/javascripts/activestorage.js b/activestorage/app/assets/javascripts/activestorage.js
new file mode 100644
index 0000000000..946217e541
--- /dev/null
+++ b/activestorage/app/assets/javascripts/activestorage.js
@@ -0,0 +1 @@
+!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ActiveStorage=e():t.ActiveStorage=e()}(this,function(){return function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={i:n,l:!1,exports:{}};return t[n].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var r={};return e.m=t,e.c=r,e.d=function(t,r,n){e.o(t,r)||Object.defineProperty(t,r,{configurable:!1,enumerable:!0,get:n})},e.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,"a",r),r},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=2)}([function(t,e,r){"use strict";function n(t){var e=a(document.head,'meta[name="'+t+'"]');if(e)return e.getAttribute("content")}function i(t,e){return"string"==typeof t&&(e=t,t=document),o(t.querySelectorAll(e))}function a(t,e){return"string"==typeof t&&(e=t,t=document),t.querySelector(e)}function u(t,e){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},n=r.bubbles,i=r.cancelable,a=r.detail,u=document.createEvent("Event");return u.initEvent(e,n||!0,i||!0),u.detail=a||{},t.dispatchEvent(u),u}function o(t){return Array.isArray(t)?t:Array.from?Array.from(t):[].slice.call(t)}e.d=n,e.c=i,e.b=a,e.a=u,e.e=o},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){if(t&&"function"==typeof t[e]){for(var r=arguments.length,n=Array(r>2?r-2:0),i=2;i<r;i++)n[i-2]=arguments[i];return t[e].apply(t,n)}}r.d(e,"a",function(){return c});var a=r(6),u=r(8),o=r(9),s=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),f=0,c=function(){function t(e,r,i){n(this,t),this.id=++f,this.file=e,this.url=r,this.delegate=i}return s(t,[{key:"create",value:function(t){var e=this;a.a.create(this.file,function(r,n){var a=new u.a(e.file,n,e.url);i(e.delegate,"directUploadWillCreateBlobWithXHR",a.xhr),a.create(function(r){if(r)t(r);else{var n=new o.a(a);i(e.delegate,"directUploadWillStoreFileWithXHR",n.xhr),n.create(function(e){e?t(e):t(null,a.toJSON())})}})})}}]),t}()},function(t,e,r){"use strict";function n(){window.ActiveStorage&&Object(i.a)()}Object.defineProperty(e,"__esModule",{value:!0});var i=r(3),a=r(1);r.d(e,"start",function(){return i.a}),r.d(e,"DirectUpload",function(){return a.a}),setTimeout(n,1)},function(t,e,r){"use strict";function n(){d||(d=!0,document.addEventListener("submit",i),document.addEventListener("ajax:before",a))}function i(t){u(t)}function a(t){"FORM"==t.target.tagName&&u(t)}function u(t){var e=t.target;if(e.hasAttribute(l))return void t.preventDefault();var r=new c.a(e),n=r.inputs;n.length&&(t.preventDefault(),e.setAttribute(l,""),n.forEach(s),r.start(function(t){e.removeAttribute(l),t?n.forEach(f):o(e)}))}function o(t){var e=Object(h.b)(t,"input[type=submit]");if(e){var r=e,n=r.disabled;e.disabled=!1,e.focus(),e.click(),e.disabled=n}else e=document.createElement("input"),e.type="submit",e.style="display:none",t.appendChild(e),e.click(),t.removeChild(e)}function s(t){t.disabled=!0}function f(t){t.disabled=!1}e.a=n;var c=r(4),h=r(0),l="data-direct-uploads-processing",d=!1},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(5),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o="input[type=file][data-direct-upload-url]:not([disabled])",s=function(){function t(e){n(this,t),this.form=e,this.inputs=Object(a.c)(e,o).filter(function(t){return t.files.length})}return u(t,[{key:"start",value:function(t){var e=this,r=this.createDirectUploadControllers();this.dispatch("start"),function n(){var i=r.shift();i?i.start(function(r){r?(t(r),e.dispatch("end")):n()}):(t(),e.dispatch("end"))}()}},{key:"createDirectUploadControllers",value:function(){var t=[];return this.inputs.forEach(function(e){Object(a.e)(e.files).forEach(function(r){var n=new i.a(e,r);t.push(n)})}),t}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return Object(a.a)(this.form,"direct-uploads:"+t,{detail:e})}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return o});var i=r(1),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=function(){function t(e,r){n(this,t),this.input=e,this.file=r,this.directUpload=new i.a(this.file,this.url,this),this.dispatch("initialize")}return u(t,[{key:"start",value:function(t){var e=this,r=document.createElement("input");r.type="hidden",r.name=this.input.name,this.input.insertAdjacentElement("beforebegin",r),this.dispatch("start"),this.directUpload.create(function(n,i){n?(r.parentNode.removeChild(r),e.dispatchError(n)):r.value=i.signed_id,e.dispatch("end"),t(n)})}},{key:"uploadRequestDidProgress",value:function(t){var e=t.loaded/t.total*100;e&&this.dispatch("progress",{progress:e})}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.file=this.file,e.id=this.directUpload.id,Object(a.a)(this.input,"direct-upload:"+t,{detail:e})}},{key:"dispatchError",value:function(t){this.dispatch("error",{error:t}).defaultPrevented||alert(t)}},{key:"directUploadWillCreateBlobWithXHR",value:function(t){this.dispatch("before-blob-request",{xhr:t})}},{key:"directUploadWillStoreFileWithXHR",value:function(t){var e=this;this.dispatch("before-storage-request",{xhr:t}),t.upload.addEventListener("progress",function(t){return e.uploadRequestDidProgress(t)})}},{key:"url",get:function(){return this.input.getAttribute("data-direct-upload-url")}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(7),a=r.n(i),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=File.prototype.slice||File.prototype.mozSlice||File.prototype.webkitSlice,s=function(){function t(e){n(this,t),this.file=e,this.chunkSize=2097152,this.chunkCount=Math.ceil(this.file.size/this.chunkSize),this.chunkIndex=0}return u(t,null,[{key:"create",value:function(e,r){new t(e).create(r)}}]),u(t,[{key:"create",value:function(t){var e=this;this.callback=t,this.md5Buffer=new a.a.ArrayBuffer,this.fileReader=new FileReader,this.fileReader.addEventListener("load",function(t){return e.fileReaderDidLoad(t)}),this.fileReader.addEventListener("error",function(t){return e.fileReaderDidError(t)}),this.readNextChunk()}},{key:"fileReaderDidLoad",value:function(t){if(this.md5Buffer.append(t.target.result),!this.readNextChunk()){var e=this.md5Buffer.end(!0),r=btoa(e);this.callback(null,r)}}},{key:"fileReaderDidError",value:function(t){this.callback("Error reading "+this.file.name)}},{key:"readNextChunk",value:function(){if(this.chunkIndex<this.chunkCount){var t=this.chunkIndex*this.chunkSize,e=Math.min(t+this.chunkSize,this.file.size),r=o.call(this.file,t,e);return this.fileReader.readAsArrayBuffer(r),this.chunkIndex++,!0}return!1}}]),t}()},function(t,e,r){!function(e){t.exports=e()}(function(t){"use strict";function e(t,e){var r=t[0],n=t[1],i=t[2],a=t[3];r+=(n&i|~n&a)+e[0]-680876936|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[1]-389564586|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[2]+606105819|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[3]-1044525330|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[4]-176418897|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[5]+1200080426|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[6]-1473231341|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[7]-45705983|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[8]+1770035416|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[9]-1958414417|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[10]-42063|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[11]-1990404162|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[12]+1804603682|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[13]-40341101|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[14]-1502002290|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[15]+1236535329|0,n=(n<<22|n>>>10)+i|0,r+=(n&a|i&~a)+e[1]-165796510|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[6]-1069501632|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[11]+643717713|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[0]-373897302|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[5]-701558691|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[10]+38016083|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[15]-660478335|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[4]-405537848|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[9]+568446438|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[14]-1019803690|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[3]-187363961|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[8]+1163531501|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[13]-1444681467|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[2]-51403784|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[7]+1735328473|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[12]-1926607734|0,n=(n<<20|n>>>12)+i|0,r+=(n^i^a)+e[5]-378558|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[8]-2022574463|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[11]+1839030562|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[14]-35309556|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[1]-1530992060|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[4]+1272893353|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[7]-155497632|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[10]-1094730640|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[13]+681279174|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[0]-358537222|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[3]-722521979|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[6]+76029189|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[9]-640364487|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[12]-421815835|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[15]+530742520|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[2]-995338651|0,n=(n<<23|n>>>9)+i|0,r+=(i^(n|~a))+e[0]-198630844|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[7]+1126891415|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[14]-1416354905|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[5]-57434055|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[12]+1700485571|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[3]-1894986606|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[10]-1051523|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[1]-2054922799|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[8]+1873313359|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[15]-30611744|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[6]-1560198380|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[13]+1309151649|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[4]-145523070|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[11]-1120210379|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[2]+718787259|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[9]-343485551|0,n=(n<<21|n>>>11)+i|0,t[0]=r+t[0]|0,t[1]=n+t[1]|0,t[2]=i+t[2]|0,t[3]=a+t[3]|0}function r(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t.charCodeAt(e)+(t.charCodeAt(e+1)<<8)+(t.charCodeAt(e+2)<<16)+(t.charCodeAt(e+3)<<24);return r}function n(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t[e]+(t[e+1]<<8)+(t[e+2]<<16)+(t[e+3]<<24);return r}function i(t){var n,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(n=64;n<=f;n+=64)e(c,r(t.substring(n-64,n)));for(t=t.substring(n-64),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],n=0;n<i;n+=1)a[n>>2]|=t.charCodeAt(n)<<(n%4<<3);if(a[n>>2]|=128<<(n%4<<3),n>55)for(e(c,a),n=0;n<16;n+=1)a[n]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function a(t){var r,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(r=64;r<=f;r+=64)e(c,n(t.subarray(r-64,r)));for(t=r-64<f?t.subarray(r-64):new Uint8Array(0),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],r=0;r<i;r+=1)a[r>>2]|=t[r]<<(r%4<<3);if(a[r>>2]|=128<<(r%4<<3),r>55)for(e(c,a),r=0;r<16;r+=1)a[r]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function u(t){var e,r="";for(e=0;e<4;e+=1)r+=p[t>>8*e+4&15]+p[t>>8*e&15];return r}function o(t){var e;for(e=0;e<t.length;e+=1)t[e]=u(t[e]);return t.join("")}function s(t){return/[\u0080-\uFFFF]/.test(t)&&(t=unescape(encodeURIComponent(t))),t}function f(t,e){var r,n=t.length,i=new ArrayBuffer(n),a=new Uint8Array(i);for(r=0;r<n;r+=1)a[r]=t.charCodeAt(r);return e?a:i}function c(t){return String.fromCharCode.apply(null,new Uint8Array(t))}function h(t,e,r){var n=new Uint8Array(t.byteLength+e.byteLength);return n.set(new Uint8Array(t)),n.set(new Uint8Array(e),t.byteLength),r?n:n.buffer}function l(t){var e,r=[],n=t.length;for(e=0;e<n-1;e+=2)r.push(parseInt(t.substr(e,2),16));return String.fromCharCode.apply(String,r)}function d(){this.reset()}var p=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];return"5d41402abc4b2a76b9719d911017c592"!==o(i("hello"))&&function(t,e){var r=(65535&t)+(65535&e);return(t>>16)+(e>>16)+(r>>16)<<16|65535&r},"undefined"==typeof ArrayBuffer||ArrayBuffer.prototype.slice||function(){function e(t,e){return t=0|t||0,t<0?Math.max(t+e,0):Math.min(t,e)}ArrayBuffer.prototype.slice=function(r,n){var i,a,u,o,s=this.byteLength,f=e(r,s),c=s;return n!==t&&(c=e(n,s)),f>c?new ArrayBuffer(0):(i=c-f,a=new ArrayBuffer(i),u=new Uint8Array(a),o=new Uint8Array(this,f,i),u.set(o),a)}}(),d.prototype.append=function(t){return this.appendBinary(s(t)),this},d.prototype.appendBinary=function(t){this._buff+=t,this._length+=t.length;var n,i=this._buff.length;for(n=64;n<=i;n+=64)e(this._hash,r(this._buff.substring(n-64,n)));return this._buff=this._buff.substring(n-64),this},d.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n.charCodeAt(e)<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.prototype.reset=function(){return this._buff="",this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.prototype.getState=function(){return{buff:this._buff,length:this._length,hash:this._hash}},d.prototype.setState=function(t){return this._buff=t.buff,this._length=t.length,this._hash=t.hash,this},d.prototype.destroy=function(){delete this._hash,delete this._buff,delete this._length},d.prototype._finish=function(t,r){var n,i,a,u=r;if(t[u>>2]|=128<<(u%4<<3),u>55)for(e(this._hash,t),u=0;u<16;u+=1)t[u]=0;n=8*this._length,n=n.toString(16).match(/(.*?)(.{0,8})$/),i=parseInt(n[2],16),a=parseInt(n[1],16)||0,t[14]=i,t[15]=a,e(this._hash,t)},d.hash=function(t,e){return d.hashBinary(s(t),e)},d.hashBinary=function(t,e){var r=i(t),n=o(r);return e?l(n):n},d.ArrayBuffer=function(){this.reset()},d.ArrayBuffer.prototype.append=function(t){var r,i=h(this._buff.buffer,t,!0),a=i.length;for(this._length+=t.byteLength,r=64;r<=a;r+=64)e(this._hash,n(i.subarray(r-64,r)));return this._buff=r-64<a?new Uint8Array(i.buffer.slice(r-64)):new Uint8Array(0),this},d.ArrayBuffer.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n[e]<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.ArrayBuffer.prototype.reset=function(){return this._buff=new Uint8Array(0),this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.ArrayBuffer.prototype.getState=function(){var t=d.prototype.getState.call(this);return t.buff=c(t.buff),t},d.ArrayBuffer.prototype.setState=function(t){return t.buff=f(t.buff,!0),d.prototype.setState.call(this,t)},d.ArrayBuffer.prototype.destroy=d.prototype.destroy,d.ArrayBuffer.prototype._finish=d.prototype._finish,d.ArrayBuffer.hash=function(t,e){var r=a(new Uint8Array(t)),n=o(r);return e?l(n):n},d})},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return u});var i=r(0),a=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),u=function(){function t(e,r,a){var u=this;n(this,t),this.file=e,this.attributes={filename:e.name,content_type:e.type,byte_size:e.size,checksum:r},this.xhr=new XMLHttpRequest,this.xhr.open("POST",a,!0),this.xhr.responseType="json",this.xhr.setRequestHeader("Content-Type","application/json"),this.xhr.setRequestHeader("Accept","application/json"),this.xhr.setRequestHeader("X-Requested-With","XMLHttpRequest"),this.xhr.setRequestHeader("X-CSRF-Token",Object(i.d)("csrf-token")),this.xhr.addEventListener("load",function(t){return u.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return u.requestDidError(t)})}return a(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(JSON.stringify({blob:this.attributes}))}},{key:"requestDidLoad",value:function(t){if(this.status>=200&&this.status<300){var e=this.response,r=e.direct_upload;delete e.direct_upload,this.attributes=e,this.directUploadData=r,this.callback(null,this.toJSON())}else this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error creating Blob for "'+this.file.name+'". Status: '+this.status)}},{key:"toJSON",value:function(){var t={};for(var e in this.attributes)t[e]=this.attributes[e];return t}},{key:"status",get:function(){return this.xhr.status}},{key:"response",get:function(){var t=this.xhr,e=t.responseType,r=t.response;return"json"==e?r:JSON.parse(r)}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return a});var i=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),a=function(){function t(e){var r=this;n(this,t),this.blob=e,this.file=e.file;var i=e.directUploadData,a=i.url,u=i.headers;this.xhr=new XMLHttpRequest,this.xhr.open("PUT",a,!0),this.xhr.responseType="text";for(var o in u)this.xhr.setRequestHeader(o,u[o]);this.xhr.addEventListener("load",function(t){return r.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return r.requestDidError(t)})}return i(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(this.file)}},{key:"requestDidLoad",value:function(t){var e=this.xhr,r=e.status,n=e.response;r>=200&&r<300?this.callback(null,n):this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error storing "'+this.file.name+'". Status: '+this.xhr.status)}}]),t}()}])}); \ No newline at end of file
diff --git a/activestorage/app/controllers/active_storage/blobs_controller.rb b/activestorage/app/controllers/active_storage/blobs_controller.rb
new file mode 100644
index 0000000000..fa44131048
--- /dev/null
+++ b/activestorage/app/controllers/active_storage/blobs_controller.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# Take a signed permanent reference for a blob and turn it into an expiring service URL for download.
+# Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
+# security-through-obscurity factor of the signed blob references, you'll need to implement your own
+# authenticated redirection controller.
+class ActiveStorage::BlobsController < ActionController::Base
+ include ActiveStorage::SetBlob
+
+ def show
+ expires_in ActiveStorage::Blob.service.url_expires_in
+ redirect_to @blob.service_url(disposition: params[:disposition])
+ end
+end
diff --git a/activestorage/app/controllers/active_storage/direct_uploads_controller.rb b/activestorage/app/controllers/active_storage/direct_uploads_controller.rb
new file mode 100644
index 0000000000..205d173648
--- /dev/null
+++ b/activestorage/app/controllers/active_storage/direct_uploads_controller.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# Creates a new blob on the server side in anticipation of a direct-to-service upload from the client side.
+# When the client-side upload is completed, the signed_blob_id can be submitted as part of the form to reference
+# the blob that was created up front.
+class ActiveStorage::DirectUploadsController < ActionController::Base
+ def create
+ blob = ActiveStorage::Blob.create_before_direct_upload!(blob_args)
+ render json: direct_upload_json(blob)
+ end
+
+ private
+ def blob_args
+ params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, :metadata).to_h.symbolize_keys
+ end
+
+ def direct_upload_json(blob)
+ blob.as_json(methods: :signed_id).merge(direct_upload: {
+ url: blob.service_url_for_direct_upload,
+ headers: blob.service_headers_for_direct_upload
+ })
+ end
+end
diff --git a/activestorage/app/controllers/active_storage/disk_controller.rb b/activestorage/app/controllers/active_storage/disk_controller.rb
new file mode 100644
index 0000000000..a7e10c0696
--- /dev/null
+++ b/activestorage/app/controllers/active_storage/disk_controller.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+# Serves files stored with the disk service in the same way that the cloud services do.
+# This means using expiring, signed URLs that are meant for immediate access, not permanent linking.
+# Always go through the BlobsController, or your own authenticated controller, rather than directly
+# to the service url.
+class ActiveStorage::DiskController < ActionController::Base
+ skip_forgery_protection if default_protect_from_forgery
+
+ def show
+ if key = decode_verified_key
+ send_data disk_service.download(key),
+ disposition: params[:disposition], content_type: params[:content_type]
+ else
+ head :not_found
+ end
+ end
+
+ def update
+ if token = decode_verified_token
+ if acceptable_content?(token)
+ disk_service.upload token[:key], request.body, checksum: token[:checksum]
+ else
+ head :unprocessable_entity
+ end
+ else
+ head :not_found
+ end
+ rescue ActiveStorage::IntegrityError
+ head :unprocessable_entity
+ end
+
+ private
+ def disk_service
+ ActiveStorage::Blob.service
+ end
+
+
+ def decode_verified_key
+ ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
+ end
+
+
+ def decode_verified_token
+ ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
+ end
+
+ def acceptable_content?(token)
+ token[:content_type] == request.content_type && token[:content_length] == request.content_length
+ end
+end
diff --git a/activestorage/app/controllers/active_storage/previews_controller.rb b/activestorage/app/controllers/active_storage/previews_controller.rb
new file mode 100644
index 0000000000..aa7ef58ca4
--- /dev/null
+++ b/activestorage/app/controllers/active_storage/previews_controller.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class ActiveStorage::PreviewsController < ActionController::Base
+ include ActiveStorage::SetBlob
+
+ def show
+ expires_in ActiveStorage::Blob.service.url_expires_in
+ redirect_to ActiveStorage::Preview.new(@blob, params[:variation_key]).processed.service_url(disposition: params[:disposition])
+ end
+end
diff --git a/activestorage/app/controllers/active_storage/variants_controller.rb b/activestorage/app/controllers/active_storage/variants_controller.rb
new file mode 100644
index 0000000000..e8f8dd592d
--- /dev/null
+++ b/activestorage/app/controllers/active_storage/variants_controller.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# Take a signed permanent reference for a variant and turn it into an expiring service URL for download.
+# Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
+# security-through-obscurity factor of the signed blob and variation reference, you'll need to implement your own
+# authenticated redirection controller.
+class ActiveStorage::VariantsController < ActionController::Base
+ include ActiveStorage::SetBlob
+
+ def show
+ expires_in ActiveStorage::Blob.service.url_expires_in
+ redirect_to ActiveStorage::Variant.new(@blob, params[:variation_key]).processed.service_url(disposition: params[:disposition])
+ end
+end
diff --git a/activestorage/app/controllers/concerns/active_storage/set_blob.rb b/activestorage/app/controllers/concerns/active_storage/set_blob.rb
new file mode 100644
index 0000000000..b0f3d97a66
--- /dev/null
+++ b/activestorage/app/controllers/concerns/active_storage/set_blob.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module ActiveStorage::SetBlob
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :set_blob
+ end
+
+ private
+ def set_blob
+ @blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id] || params[:signed_id])
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
+ head :not_found
+ end
+end
diff --git a/activestorage/app/javascript/activestorage/blob_record.js b/activestorage/app/javascript/activestorage/blob_record.js
new file mode 100644
index 0000000000..ff847892b2
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/blob_record.js
@@ -0,0 +1,68 @@
+import { getMetaValue } from "./helpers"
+
+export class BlobRecord {
+ constructor(file, checksum, url) {
+ this.file = file
+
+ this.attributes = {
+ filename: file.name,
+ content_type: file.type,
+ byte_size: file.size,
+ checksum: checksum
+ }
+
+ this.xhr = new XMLHttpRequest
+ this.xhr.open("POST", url, true)
+ this.xhr.responseType = "json"
+ this.xhr.setRequestHeader("Content-Type", "application/json")
+ this.xhr.setRequestHeader("Accept", "application/json")
+ this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest")
+ this.xhr.setRequestHeader("X-CSRF-Token", getMetaValue("csrf-token"))
+ this.xhr.addEventListener("load", event => this.requestDidLoad(event))
+ this.xhr.addEventListener("error", event => this.requestDidError(event))
+ }
+
+ get status() {
+ return this.xhr.status
+ }
+
+ get response() {
+ const { responseType, response } = this.xhr
+ if (responseType == "json") {
+ return response
+ } else {
+ // Shim for IE 11: https://connect.microsoft.com/IE/feedback/details/794808
+ return JSON.parse(response)
+ }
+ }
+
+ create(callback) {
+ this.callback = callback
+ this.xhr.send(JSON.stringify({ blob: this.attributes }))
+ }
+
+ requestDidLoad(event) {
+ if (this.status >= 200 && this.status < 300) {
+ const { response } = this
+ const { direct_upload } = response
+ delete response.direct_upload
+ this.attributes = response
+ this.directUploadData = direct_upload
+ this.callback(null, this.toJSON())
+ } else {
+ this.requestDidError(event)
+ }
+ }
+
+ requestDidError(event) {
+ this.callback(`Error creating Blob for "${this.file.name}". Status: ${this.status}`)
+ }
+
+ toJSON() {
+ const result = {}
+ for (const key in this.attributes) {
+ result[key] = this.attributes[key]
+ }
+ return result
+ }
+}
diff --git a/activestorage/app/javascript/activestorage/blob_upload.js b/activestorage/app/javascript/activestorage/blob_upload.js
new file mode 100644
index 0000000000..180a7415e7
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/blob_upload.js
@@ -0,0 +1,35 @@
+export class BlobUpload {
+ constructor(blob) {
+ this.blob = blob
+ this.file = blob.file
+
+ const { url, headers } = blob.directUploadData
+
+ this.xhr = new XMLHttpRequest
+ this.xhr.open("PUT", url, true)
+ this.xhr.responseType = "text"
+ for (const key in headers) {
+ this.xhr.setRequestHeader(key, headers[key])
+ }
+ this.xhr.addEventListener("load", event => this.requestDidLoad(event))
+ this.xhr.addEventListener("error", event => this.requestDidError(event))
+ }
+
+ create(callback) {
+ this.callback = callback
+ this.xhr.send(this.file)
+ }
+
+ requestDidLoad(event) {
+ const { status, response } = this.xhr
+ if (status >= 200 && status < 300) {
+ this.callback(null, response)
+ } else {
+ this.requestDidError(event)
+ }
+ }
+
+ requestDidError(event) {
+ this.callback(`Error storing "${this.file.name}". Status: ${this.xhr.status}`)
+ }
+}
diff --git a/activestorage/app/javascript/activestorage/direct_upload.js b/activestorage/app/javascript/activestorage/direct_upload.js
new file mode 100644
index 0000000000..7085e0a4ab
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/direct_upload.js
@@ -0,0 +1,42 @@
+import { FileChecksum } from "./file_checksum"
+import { BlobRecord } from "./blob_record"
+import { BlobUpload } from "./blob_upload"
+
+let id = 0
+
+export class DirectUpload {
+ constructor(file, url, delegate) {
+ this.id = ++id
+ this.file = file
+ this.url = url
+ this.delegate = delegate
+ }
+
+ create(callback) {
+ FileChecksum.create(this.file, (error, checksum) => {
+ const blob = new BlobRecord(this.file, checksum, this.url)
+ notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr)
+ blob.create(error => {
+ if (error) {
+ callback(error)
+ } else {
+ const upload = new BlobUpload(blob)
+ notify(this.delegate, "directUploadWillStoreFileWithXHR", upload.xhr)
+ upload.create(error => {
+ if (error) {
+ callback(error)
+ } else {
+ callback(null, blob.toJSON())
+ }
+ })
+ }
+ })
+ })
+ }
+}
+
+function notify(object, methodName, ...messages) {
+ if (object && typeof object[methodName] == "function") {
+ return object[methodName](...messages)
+ }
+}
diff --git a/activestorage/app/javascript/activestorage/direct_upload_controller.js b/activestorage/app/javascript/activestorage/direct_upload_controller.js
new file mode 100644
index 0000000000..987050889a
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/direct_upload_controller.js
@@ -0,0 +1,67 @@
+import { DirectUpload } from "./direct_upload"
+import { dispatchEvent } from "./helpers"
+
+export class DirectUploadController {
+ constructor(input, file) {
+ this.input = input
+ this.file = file
+ this.directUpload = new DirectUpload(this.file, this.url, this)
+ this.dispatch("initialize")
+ }
+
+ start(callback) {
+ const hiddenInput = document.createElement("input")
+ hiddenInput.type = "hidden"
+ hiddenInput.name = this.input.name
+ this.input.insertAdjacentElement("beforebegin", hiddenInput)
+
+ this.dispatch("start")
+
+ this.directUpload.create((error, attributes) => {
+ if (error) {
+ hiddenInput.parentNode.removeChild(hiddenInput)
+ this.dispatchError(error)
+ } else {
+ hiddenInput.value = attributes.signed_id
+ }
+
+ this.dispatch("end")
+ callback(error)
+ })
+ }
+
+ uploadRequestDidProgress(event) {
+ const progress = event.loaded / event.total * 100
+ if (progress) {
+ this.dispatch("progress", { progress })
+ }
+ }
+
+ get url() {
+ return this.input.getAttribute("data-direct-upload-url")
+ }
+
+ dispatch(name, detail = {}) {
+ detail.file = this.file
+ detail.id = this.directUpload.id
+ return dispatchEvent(this.input, `direct-upload:${name}`, { detail })
+ }
+
+ dispatchError(error) {
+ const event = this.dispatch("error", { error })
+ if (!event.defaultPrevented) {
+ alert(error)
+ }
+ }
+
+ // DirectUpload delegate
+
+ directUploadWillCreateBlobWithXHR(xhr) {
+ this.dispatch("before-blob-request", { xhr })
+ }
+
+ directUploadWillStoreFileWithXHR(xhr) {
+ this.dispatch("before-storage-request", { xhr })
+ xhr.upload.addEventListener("progress", event => this.uploadRequestDidProgress(event))
+ }
+}
diff --git a/activestorage/app/javascript/activestorage/direct_uploads_controller.js b/activestorage/app/javascript/activestorage/direct_uploads_controller.js
new file mode 100644
index 0000000000..94b89c9119
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/direct_uploads_controller.js
@@ -0,0 +1,50 @@
+import { DirectUploadController } from "./direct_upload_controller"
+import { findElements, dispatchEvent, toArray } from "./helpers"
+
+const inputSelector = "input[type=file][data-direct-upload-url]:not([disabled])"
+
+export class DirectUploadsController {
+ constructor(form) {
+ this.form = form
+ this.inputs = findElements(form, inputSelector).filter(input => input.files.length)
+ }
+
+ start(callback) {
+ const controllers = this.createDirectUploadControllers()
+
+ const startNextController = () => {
+ const controller = controllers.shift()
+ if (controller) {
+ controller.start(error => {
+ if (error) {
+ callback(error)
+ this.dispatch("end")
+ } else {
+ startNextController()
+ }
+ })
+ } else {
+ callback()
+ this.dispatch("end")
+ }
+ }
+
+ this.dispatch("start")
+ startNextController()
+ }
+
+ createDirectUploadControllers() {
+ const controllers = []
+ this.inputs.forEach(input => {
+ toArray(input.files).forEach(file => {
+ const controller = new DirectUploadController(input, file)
+ controllers.push(controller)
+ })
+ })
+ return controllers
+ }
+
+ dispatch(name, detail = {}) {
+ return dispatchEvent(this.form, `direct-uploads:${name}`, { detail })
+ }
+}
diff --git a/activestorage/app/javascript/activestorage/file_checksum.js b/activestorage/app/javascript/activestorage/file_checksum.js
new file mode 100644
index 0000000000..ffaec1a128
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/file_checksum.js
@@ -0,0 +1,53 @@
+import SparkMD5 from "spark-md5"
+
+const fileSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
+
+export class FileChecksum {
+ static create(file, callback) {
+ const instance = new FileChecksum(file)
+ instance.create(callback)
+ }
+
+ constructor(file) {
+ this.file = file
+ this.chunkSize = 2097152 // 2MB
+ this.chunkCount = Math.ceil(this.file.size / this.chunkSize)
+ this.chunkIndex = 0
+ }
+
+ create(callback) {
+ this.callback = callback
+ this.md5Buffer = new SparkMD5.ArrayBuffer
+ this.fileReader = new FileReader
+ this.fileReader.addEventListener("load", event => this.fileReaderDidLoad(event))
+ this.fileReader.addEventListener("error", event => this.fileReaderDidError(event))
+ this.readNextChunk()
+ }
+
+ fileReaderDidLoad(event) {
+ this.md5Buffer.append(event.target.result)
+
+ if (!this.readNextChunk()) {
+ const binaryDigest = this.md5Buffer.end(true)
+ const base64digest = btoa(binaryDigest)
+ this.callback(null, base64digest)
+ }
+ }
+
+ fileReaderDidError(event) {
+ this.callback(`Error reading ${this.file.name}`)
+ }
+
+ readNextChunk() {
+ if (this.chunkIndex < this.chunkCount) {
+ const start = this.chunkIndex * this.chunkSize
+ const end = Math.min(start + this.chunkSize, this.file.size)
+ const bytes = fileSlice.call(this.file, start, end)
+ this.fileReader.readAsArrayBuffer(bytes)
+ this.chunkIndex++
+ return true
+ } else {
+ return false
+ }
+ }
+}
diff --git a/activestorage/app/javascript/activestorage/helpers.js b/activestorage/app/javascript/activestorage/helpers.js
new file mode 100644
index 0000000000..52fec8f6f1
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/helpers.js
@@ -0,0 +1,42 @@
+export function getMetaValue(name) {
+ const element = findElement(document.head, `meta[name="${name}"]`)
+ if (element) {
+ return element.getAttribute("content")
+ }
+}
+
+export function findElements(root, selector) {
+ if (typeof root == "string") {
+ selector = root
+ root = document
+ }
+ const elements = root.querySelectorAll(selector)
+ return toArray(elements)
+}
+
+export function findElement(root, selector) {
+ if (typeof root == "string") {
+ selector = root
+ root = document
+ }
+ return root.querySelector(selector)
+}
+
+export function dispatchEvent(element, type, eventInit = {}) {
+ const { bubbles, cancelable, detail } = eventInit
+ const event = document.createEvent("Event")
+ event.initEvent(type, bubbles || true, cancelable || true)
+ event.detail = detail || {}
+ element.dispatchEvent(event)
+ return event
+}
+
+export function toArray(value) {
+ if (Array.isArray(value)) {
+ return value
+ } else if (Array.from) {
+ return Array.from(value)
+ } else {
+ return [].slice.call(value)
+ }
+}
diff --git a/activestorage/app/javascript/activestorage/index.js b/activestorage/app/javascript/activestorage/index.js
new file mode 100644
index 0000000000..a340008fb9
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/index.js
@@ -0,0 +1,11 @@
+import { start } from "./ujs"
+import { DirectUpload } from "./direct_upload"
+export { start, DirectUpload }
+
+function autostart() {
+ if (window.ActiveStorage) {
+ start()
+ }
+}
+
+setTimeout(autostart, 1)
diff --git a/activestorage/app/javascript/activestorage/ujs.js b/activestorage/app/javascript/activestorage/ujs.js
new file mode 100644
index 0000000000..1dda02936f
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/ujs.js
@@ -0,0 +1,75 @@
+import { DirectUploadsController } from "./direct_uploads_controller"
+import { findElement } from "./helpers"
+
+const processingAttribute = "data-direct-uploads-processing"
+let started = false
+
+export function start() {
+ if (!started) {
+ started = true
+ document.addEventListener("submit", didSubmitForm)
+ document.addEventListener("ajax:before", didSubmitRemoteElement)
+ }
+}
+
+function didSubmitForm(event) {
+ handleFormSubmissionEvent(event)
+}
+
+function didSubmitRemoteElement(event) {
+ if (event.target.tagName == "FORM") {
+ handleFormSubmissionEvent(event)
+ }
+}
+
+function handleFormSubmissionEvent(event) {
+ const form = event.target
+
+ if (form.hasAttribute(processingAttribute)) {
+ event.preventDefault()
+ return
+ }
+
+ const controller = new DirectUploadsController(form)
+ const { inputs } = controller
+
+ if (inputs.length) {
+ event.preventDefault()
+ form.setAttribute(processingAttribute, "")
+ inputs.forEach(disable)
+ controller.start(error => {
+ form.removeAttribute(processingAttribute)
+ if (error) {
+ inputs.forEach(enable)
+ } else {
+ submitForm(form)
+ }
+ })
+ }
+}
+
+function submitForm(form) {
+ let button = findElement(form, "input[type=submit]")
+ if (button) {
+ const { disabled } = button
+ button.disabled = false
+ button.focus()
+ button.click()
+ button.disabled = disabled
+ } else {
+ button = document.createElement("input")
+ button.type = "submit"
+ button.style = "display:none"
+ form.appendChild(button)
+ button.click()
+ form.removeChild(button)
+ }
+}
+
+function disable(input) {
+ input.disabled = true
+}
+
+function enable(input) {
+ input.disabled = false
+}
diff --git a/activestorage/app/jobs/active_storage/analyze_job.rb b/activestorage/app/jobs/active_storage/analyze_job.rb
new file mode 100644
index 0000000000..2a952f9f74
--- /dev/null
+++ b/activestorage/app/jobs/active_storage/analyze_job.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+# Provides asynchronous analysis of ActiveStorage::Blob records via ActiveStorage::Blob#analyze_later.
+class ActiveStorage::AnalyzeJob < ActiveStorage::BaseJob
+ def perform(blob)
+ blob.analyze
+ end
+end
diff --git a/activestorage/app/jobs/active_storage/base_job.rb b/activestorage/app/jobs/active_storage/base_job.rb
new file mode 100644
index 0000000000..6caab42a2d
--- /dev/null
+++ b/activestorage/app/jobs/active_storage/base_job.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ActiveStorage::BaseJob < ActiveJob::Base
+ queue_as { ActiveStorage.queue }
+end
diff --git a/activestorage/app/jobs/active_storage/purge_job.rb b/activestorage/app/jobs/active_storage/purge_job.rb
new file mode 100644
index 0000000000..98874d2250
--- /dev/null
+++ b/activestorage/app/jobs/active_storage/purge_job.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+# Provides asynchronous purging of ActiveStorage::Blob records via ActiveStorage::Blob#purge_later.
+class ActiveStorage::PurgeJob < ActiveStorage::BaseJob
+ # FIXME: Limit this to a custom ActiveStorage error
+ retry_on StandardError
+
+ def perform(blob)
+ blob.purge
+ end
+end
diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb
new file mode 100644
index 0000000000..9f61a5dbf3
--- /dev/null
+++ b/activestorage/app/models/active_storage/attachment.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/delegation"
+
+# Attachments associate records with blobs. Usually that's a one record-many blobs relationship,
+# but it is possible to associate many different records with the same blob. If you're doing that,
+# you'll want to declare with <tt>has_one/many_attached :thingy, dependent: false</tt>, so that destroying
+# any one record won't destroy the blob as well. (Then you'll need to do your own garbage collecting, though).
+class ActiveStorage::Attachment < ActiveRecord::Base
+ self.table_name = "active_storage_attachments"
+
+ belongs_to :record, polymorphic: true, touch: true
+ belongs_to :blob, class_name: "ActiveStorage::Blob"
+
+ delegate_missing_to :blob
+
+ after_create_commit :analyze_blob_later
+
+ # Synchronously purges the blob (deletes it from the configured service) and destroys the attachment.
+ def purge
+ blob.purge
+ destroy
+ end
+
+ # Destroys the attachment and asynchronously purges the blob (deletes it from the configured service).
+ def purge_later
+ blob.purge_later
+ destroy
+ end
+
+ private
+ def analyze_blob_later
+ blob.analyze_later unless blob.analyzed?
+ end
+end
diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb
new file mode 100644
index 0000000000..3b48ee72af
--- /dev/null
+++ b/activestorage/app/models/active_storage/blob.rb
@@ -0,0 +1,328 @@
+# frozen_string_literal: true
+
+require "active_storage/analyzer/null_analyzer"
+
+# A blob is a record that contains the metadata about a file and a key for where that file resides on the service.
+# Blobs can be created in two ways:
+#
+# 1. Subsequent to the file being uploaded server-side to the service via <tt>create_after_upload!</tt>.
+# 2. Ahead of the file being directly uploaded client-side to the service via <tt>create_before_direct_upload!</tt>.
+#
+# The first option doesn't require any client-side JavaScript integration, and can be used by any other back-end
+# service that deals with files. The second option is faster, since you're not using your own server as a staging
+# point for uploads, and can work with deployments like Heroku that do not provide large amounts of disk space.
+#
+# Blobs are intended to be immutable in as-so-far as their reference to a specific file goes. You're allowed to
+# update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
+# If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
+class ActiveStorage::Blob < ActiveRecord::Base
+ class InvariableError < StandardError; end
+ class UnpreviewableError < StandardError; end
+ class UnrepresentableError < StandardError; end
+
+ self.table_name = "active_storage_blobs"
+
+ has_secure_token :key
+ store :metadata, accessors: [ :analyzed ], coder: JSON
+
+ class_attribute :service
+
+ has_many :attachments
+
+ has_one_attached :preview_image
+
+ class << self
+ # You can used the signed ID of a blob to refer to it on the client side without fear of tampering.
+ # This is particularly helpful for direct uploads where the client-side needs to refer to the blob
+ # that was created ahead of the upload itself on form submission.
+ #
+ # The signed ID is also used to create stable URLs for the blob through the BlobsController.
+ def find_signed(id)
+ find ActiveStorage.verifier.verify(id, purpose: :blob_id)
+ end
+
+ # Returns a new, unsaved blob instance after the +io+ has been uploaded to the service.
+ def build_after_upload(io:, filename:, content_type: nil, metadata: nil)
+ new.tap do |blob|
+ blob.filename = filename
+ blob.content_type = content_type
+ blob.metadata = metadata
+
+ blob.upload io
+ end
+ end
+
+ # Returns a saved blob instance after the +io+ has been uploaded to the service. Note, the blob is first built,
+ # then the +io+ is uploaded, then the blob is saved. This is done this way to avoid uploading (which may take
+ # time), while having an open database transaction.
+ def create_after_upload!(io:, filename:, content_type: nil, metadata: nil)
+ build_after_upload(io: io, filename: filename, content_type: content_type, metadata: metadata).tap(&:save!)
+ end
+
+ # Returns a saved blob _without_ uploading a file to the service. This blob will point to a key where there is
+ # no file yet. It's intended to be used together with a client-side upload, which will first create the blob
+ # in order to produce the signed URL for uploading. This signed URL points to the key generated by the blob.
+ # Once the form using the direct upload is submitted, the blob can be associated with the right record using
+ # the signed ID.
+ def create_before_direct_upload!(filename:, byte_size:, checksum:, content_type: nil, metadata: nil)
+ create! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata
+ end
+ end
+
+
+ # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering.
+ # It uses the framework-wide verifier on <tt>ActiveStorage.verifier</tt>, but with a dedicated purpose.
+ def signed_id
+ ActiveStorage.verifier.generate(id, purpose: :blob_id)
+ end
+
+ # Returns the key pointing to the file on the service that's associated with this blob. The key is in the
+ # standard secure-token format from Rails. So it'll look like: XTAPjJCJiuDrLk3TmwyJGpUo. This key is not intended
+ # to be revealed directly to the user. Always refer to blobs using the signed_id or a verified form of the key.
+ def key
+ # We can't wait until the record is first saved to have a key for it
+ self[:key] ||= self.class.generate_unique_secure_token
+ end
+
+ # Returns an ActiveStorage::Filename instance of the filename that can be
+ # queried for basename, extension, and a sanitized version of the filename
+ # that's safe to use in URLs.
+ def filename
+ ActiveStorage::Filename.new(self[:filename])
+ end
+
+ # Returns true if the content_type of this blob is in the image range, like image/png.
+ def image?
+ content_type.start_with?("image")
+ end
+
+ # Returns true if the content_type of this blob is in the audio range, like audio/mpeg.
+ def audio?
+ content_type.start_with?("audio")
+ end
+
+ # Returns true if the content_type of this blob is in the video range, like video/mp4.
+ def video?
+ content_type.start_with?("video")
+ end
+
+ # Returns true if the content_type of this blob is in the text range, like text/plain.
+ def text?
+ content_type.start_with?("text")
+ end
+
+
+ # Returns an ActiveStorage::Variant instance with the set of +transformations+ provided. This is only relevant for image
+ # files, and it allows any image to be transformed for size, colors, and the like. Example:
+ #
+ # avatar.variant(resize: "100x100").processed.service_url
+ #
+ # This will create and process a variant of the avatar blob that's constrained to a height and width of 100px.
+ # Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
+ #
+ # Frequently, though, you don't actually want to transform the variant right away. But rather simply refer to a
+ # specific variant that can be created by a controller on-demand. Like so:
+ #
+ # <%= image_tag Current.user.avatar.variant(resize: "100x100") %>
+ #
+ # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController
+ # can then produce on-demand.
+ #
+ # Raises ActiveStorage::Blob::InvariableError if ImageMagick cannot transform the blob. To determine whether a blob is
+ # variable, call ActiveStorage::Blob#previewable?.
+ def variant(transformations)
+ if variable?
+ ActiveStorage::Variant.new(self, ActiveStorage::Variation.wrap(transformations))
+ else
+ raise InvariableError
+ end
+ end
+
+ # Returns true if ImageMagick can transform the blob (its content type is in +ActiveStorage.variable_content_types+).
+ def variable?
+ ActiveStorage.variable_content_types.include?(content_type)
+ end
+
+
+ # Returns an ActiveStorage::Preview instance with the set of +transformations+ provided. A preview is an image generated
+ # from a non-image blob. Active Storage comes with built-in previewers for videos and PDF documents. The video previewer
+ # extracts the first frame from a video and the PDF previewer extracts the first page from a PDF document.
+ #
+ # blob.preview(resize: "100x100").processed.service_url
+ #
+ # Avoid processing previews synchronously in views. Instead, link to a controller action that processes them on demand.
+ # Active Storage provides one, but you may want to create your own (for example, if you need authentication). Here’s
+ # how to use the built-in version:
+ #
+ # <%= image_tag video.preview(resize: "100x100") %>
+ #
+ # This method raises ActiveStorage::Blob::UnpreviewableError if no previewer accepts the receiving blob. To determine
+ # whether a blob is accepted by any previewer, call ActiveStorage::Blob#previewable?.
+ def preview(transformations)
+ if previewable?
+ ActiveStorage::Preview.new(self, ActiveStorage::Variation.wrap(transformations))
+ else
+ raise UnpreviewableError
+ end
+ end
+
+ # Returns true if any registered previewer accepts the blob. By default, this will return true for videos and PDF documents.
+ def previewable?
+ ActiveStorage.previewers.any? { |klass| klass.accept?(self) }
+ end
+
+
+ # Returns an ActiveStorage::Preview for a previewable blob or an ActiveStorage::Variant for a variable image blob.
+ #
+ # blob.representation(resize: "100x100").processed.service_url
+ #
+ # Raises ActiveStorage::Blob::UnrepresentableError if the receiving blob is neither variable nor previewable. Call
+ # ActiveStorage::Blob#representable? to determine whether a blob is representable.
+ #
+ # See ActiveStorage::Blob#preview and ActiveStorage::Blob#variant for more information.
+ def representation(transformations)
+ case
+ when previewable?
+ preview transformations
+ when variable?
+ variant transformations
+ else
+ raise UnrepresentableError
+ end
+ end
+
+ # Returns true if the blob is variable or previewable.
+ def representable?
+ variable? || previewable?
+ end
+
+
+ # Returns the URL of the blob on the service. This URL is intended to be short-lived for security and not used directly
+ # with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
+ # Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
+ # it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
+ def service_url(expires_in: service.url_expires_in, disposition: "inline")
+ service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
+ end
+
+ # Returns a URL that can be used to directly upload a file for this blob on the service. This URL is intended to be
+ # short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading.
+ def service_url_for_direct_upload(expires_in: service.url_expires_in)
+ service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum
+ end
+
+ # Returns a Hash of headers for +service_url_for_direct_upload+ requests.
+ def service_headers_for_direct_upload
+ service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum
+ end
+
+ # Uploads the +io+ to the service on the +key+ for this blob. Blobs are intended to be immutable, so you shouldn't be
+ # using this method after a file has already been uploaded to fit with a blob. If you want to create a derivative blob,
+ # you should instead simply create a new blob based on the old one.
+ #
+ # Prior to uploading, we compute the checksum, which is sent to the service for transit integrity validation. If the
+ # checksum does not match what the service receives, an exception will be raised. We also measure the size of the +io+
+ # and store that in +byte_size+ on the blob record.
+ #
+ # Normally, you do not have to call this method directly at all. Use the factory class methods of +build_after_upload+
+ # and +create_after_upload!+.
+ def upload(io)
+ self.checksum = compute_checksum_in_chunks(io)
+ self.byte_size = io.size
+
+ service.upload(key, io, checksum: checksum)
+ end
+
+ # Downloads the file associated with this blob. If no block is given, the entire file is read into memory and returned.
+ # That'll use a lot of RAM for very large files. If a block is given, then the download is streamed and yielded in chunks.
+ def download(&block)
+ service.download key, &block
+ end
+
+
+ # Extracts and stores metadata from the file associated with this blob using a relevant analyzer. Active Storage comes
+ # with built-in analyzers for images and videos. See ActiveStorage::Analyzer::ImageAnalyzer and
+ # ActiveStorage::Analyzer::VideoAnalyzer for information about the specific attributes they extract and the third-party
+ # libraries they require.
+ #
+ # To choose the analyzer for a blob, Active Storage calls +accept?+ on each registered analyzer in order. It uses the
+ # first analyzer for which +accept?+ returns true when given the blob. If no registered analyzer accepts the blob, no
+ # metadata is extracted from it.
+ #
+ # In a Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+
+ # in an initializer:
+ #
+ # # Add a custom analyzer for Microsoft Office documents:
+ # Rails.application.config.active_storage.analyzers.append DOCXAnalyzer
+ #
+ # # Remove the built-in video analyzer:
+ # Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer
+ #
+ # Outside of a Rails application, manipulate +ActiveStorage.analyzers+ instead.
+ #
+ # You won't ordinarily need to call this method from a Rails application. New blobs are automatically and asynchronously
+ # analyzed via #analyze_later when they're attached for the first time.
+ def analyze
+ update! metadata: metadata.merge(extract_metadata_via_analyzer)
+ end
+
+ # Enqueues an ActiveStorage::AnalyzeJob which calls #analyze.
+ #
+ # This method is automatically called for a blob when it's attached for the first time. You can call it to analyze a blob
+ # again (e.g. if you add a new analyzer or modify an existing one).
+ def analyze_later
+ ActiveStorage::AnalyzeJob.perform_later(self)
+ end
+
+ # Returns true if the blob has been analyzed.
+ def analyzed?
+ analyzed
+ end
+
+
+ # Deletes the file on the service that's associated with this blob. This should only be done if the blob is going to be
+ # deleted as well or you will essentially have a dead reference. It's recommended to use the +#purge+ and +#purge_later+
+ # methods in most circumstances.
+ def delete
+ service.delete(key)
+ service.delete_prefixed("variants/#{key}/") if image?
+ end
+
+ # Deletes the file on the service and then destroys the blob record. This is the recommended way to dispose of unwanted
+ # blobs. Note, though, that deleting the file off the service will initiate a HTTP connection to the service, which may
+ # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use +#purge_later+ instead.
+ def purge
+ delete
+ destroy
+ end
+
+ # Enqueues an ActiveStorage::PurgeJob job that'll call +purge+. This is the recommended way to purge blobs when the call
+ # needs to be made from a transaction, a callback, or any other real-time scenario.
+ def purge_later
+ ActiveStorage::PurgeJob.perform_later(self)
+ end
+
+ private
+ def compute_checksum_in_chunks(io)
+ Digest::MD5.new.tap do |checksum|
+ while chunk = io.read(5.megabytes)
+ checksum << chunk
+ end
+
+ io.rewind
+ end.base64digest
+ end
+
+
+ def extract_metadata_via_analyzer
+ analyzer.metadata.merge(analyzed: true)
+ end
+
+ def analyzer
+ analyzer_class.new(self)
+ end
+
+ def analyzer_class
+ ActiveStorage.analyzers.detect { |klass| klass.accept?(self) } || ActiveStorage::Analyzer::NullAnalyzer
+ end
+end
diff --git a/activestorage/app/models/active_storage/filename.rb b/activestorage/app/models/active_storage/filename.rb
new file mode 100644
index 0000000000..b9413dec95
--- /dev/null
+++ b/activestorage/app/models/active_storage/filename.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+# Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization.
+# A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting.
+class ActiveStorage::Filename
+ include Comparable
+
+ def initialize(filename)
+ @filename = filename
+ end
+
+ # Returns the part of the filename preceding any extension.
+ #
+ # ActiveStorage::Filename.new("racecar.jpg").base # => "racecar"
+ # ActiveStorage::Filename.new("racecar").base # => "racecar"
+ # ActiveStorage::Filename.new(".gitignore").base # => ".gitignore"
+ def base
+ File.basename @filename, extension_with_delimiter
+ end
+
+ # Returns the extension of the filename (i.e. the substring following the last dot, excluding a dot at the
+ # beginning) with the dot that precedes it. If the filename has no extension, an empty string is returned.
+ #
+ # ActiveStorage::Filename.new("racecar.jpg").extension_with_delimiter # => ".jpg"
+ # ActiveStorage::Filename.new("racecar").extension_with_delimiter # => ""
+ # ActiveStorage::Filename.new(".gitignore").extension_with_delimiter # => ""
+ def extension_with_delimiter
+ File.extname @filename
+ end
+
+ # Returns the extension of the filename (i.e. the substring following the last dot, excluding a dot at
+ # the beginning). If the filename has no extension, an empty string is returned.
+ #
+ # ActiveStorage::Filename.new("racecar.jpg").extension_without_delimiter # => "jpg"
+ # ActiveStorage::Filename.new("racecar").extension_without_delimiter # => ""
+ # ActiveStorage::Filename.new(".gitignore").extension_without_delimiter # => ""
+ def extension_without_delimiter
+ extension_with_delimiter.from(1).to_s
+ end
+
+ alias_method :extension, :extension_without_delimiter
+
+ # Returns the sanitized filename.
+ #
+ # ActiveStorage::Filename.new("foo:bar.jpg").sanitized # => "foo-bar.jpg"
+ # ActiveStorage::Filename.new("foo/bar.jpg").sanitized # => "foo-bar.jpg"
+ #
+ # Characters considered unsafe for storage (e.g. \, $, and the RTL override character) are replaced with a dash.
+ def sanitized
+ @filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
+ end
+
+ def parameters #:nodoc:
+ Parameters.new self
+ end
+
+ # Returns the sanitized version of the filename.
+ def to_s
+ sanitized.to_s
+ end
+
+ def as_json(*)
+ to_s
+ end
+
+ def to_json
+ to_s
+ end
+
+ def <=>(other)
+ to_s.downcase <=> other.to_s.downcase
+ end
+end
diff --git a/activestorage/app/models/active_storage/filename/parameters.rb b/activestorage/app/models/active_storage/filename/parameters.rb
new file mode 100644
index 0000000000..fb9ea10e49
--- /dev/null
+++ b/activestorage/app/models/active_storage/filename/parameters.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class ActiveStorage::Filename::Parameters #:nodoc:
+ attr_reader :filename
+
+ def initialize(filename)
+ @filename = filename
+ end
+
+ def combined
+ "#{ascii}; #{utf8}"
+ end
+
+ TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!#$+.^_`|~-]/
+
+ def ascii
+ 'filename="' + percent_escape(I18n.transliterate(filename.sanitized), TRADITIONAL_ESCAPED_CHAR) + '"'
+ end
+
+ RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!#$&+.^_`|~-]/
+
+ def utf8
+ "filename*=UTF-8''" + percent_escape(filename.sanitized, RFC_5987_ESCAPED_CHAR)
+ end
+
+ def to_s
+ combined
+ end
+
+ private
+ def percent_escape(string, pattern)
+ string.gsub(pattern) do |char|
+ char.bytes.map { |byte| "%%%02X" % byte }.join
+ end
+ end
+end
diff --git a/activestorage/app/models/active_storage/preview.rb b/activestorage/app/models/active_storage/preview.rb
new file mode 100644
index 0000000000..be5053edae
--- /dev/null
+++ b/activestorage/app/models/active_storage/preview.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+# Some non-image blobs can be previewed: that is, they can be presented as images. A video blob can be previewed by
+# extracting its first frame, and a PDF blob can be previewed by extracting its first page.
+#
+# A previewer extracts a preview image from a blob. Active Storage provides previewers for videos and PDFs:
+# ActiveStorage::Previewer::VideoPreviewer and ActiveStorage::Previewer::PDFPreviewer. Build custom previewers by
+# subclassing ActiveStorage::Previewer and implementing the requisite methods. Consult the ActiveStorage::Previewer
+# documentation for more details on what's required of previewers.
+#
+# To choose the previewer for a blob, Active Storage calls +accept?+ on each registered previewer in order. It uses the
+# first previewer for which +accept?+ returns true when given the blob. In a Rails application, add or remove previewers
+# by manipulating +Rails.application.config.active_storage.previewers+ in an initializer:
+#
+# Rails.application.config.active_storage.previewers
+# # => [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
+#
+# # Add a custom previewer for Microsoft Office documents:
+# Rails.application.config.active_storage.previewers << DOCXPreviewer
+# # => [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer, DOCXPreviewer ]
+#
+# Outside of a Rails application, modify +ActiveStorage.previewers+ instead.
+#
+# The built-in previewers rely on third-party system libraries:
+#
+# * {ffmpeg}[https://www.ffmpeg.org]
+# * {mupdf}[https://mupdf.com]
+#
+# These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you
+# install and use third-party software, make sure you understand the licensing implications of doing so.
+class ActiveStorage::Preview
+ class UnprocessedError < StandardError; end
+
+ attr_reader :blob, :variation
+
+ def initialize(blob, variation_or_variation_key)
+ @blob, @variation = blob, ActiveStorage::Variation.wrap(variation_or_variation_key)
+ end
+
+ # Processes the preview if it has not been processed yet. Returns the receiving Preview instance for convenience:
+ #
+ # blob.preview(resize: "100x100").processed.service_url
+ #
+ # Processing a preview generates an image from its blob and attaches the preview image to the blob. Because the preview
+ # image is stored with the blob, it is only generated once.
+ def processed
+ process unless processed?
+ self
+ end
+
+ # Returns the blob's attached preview image.
+ def image
+ blob.preview_image
+ end
+
+ # Returns the URL of the preview's variant on the service. Raises ActiveStorage::Preview::UnprocessedError if the
+ # preview has not been processed yet.
+ #
+ # This method synchronously processes a variant of the preview image, so do not call it in views. Instead, generate
+ # a stable URL that redirects to the short-lived URL returned by this method.
+ def service_url(**options)
+ if processed?
+ variant.service_url(options)
+ else
+ raise UnprocessedError
+ end
+ end
+
+ private
+ def processed?
+ image.attached?
+ end
+
+ def process
+ previewer.preview { |attachable| image.attach(attachable) }
+ end
+
+ def variant
+ ActiveStorage::Variant.new(image, variation).processed
+ end
+
+
+ def previewer
+ previewer_class.new(blob)
+ end
+
+ def previewer_class
+ ActiveStorage.previewers.detect { |klass| klass.accept?(blob) }
+ end
+end
diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb
new file mode 100644
index 0000000000..e08a2271ec
--- /dev/null
+++ b/activestorage/app/models/active_storage/variant.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+require "active_storage/downloading"
+
+# Image blobs can have variants that are the result of a set of transformations applied to the original.
+# These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the
+# original.
+#
+# Variants rely on {MiniMagick}[https://github.com/minimagick/minimagick] for the actual transformations
+# of the file, so you must add <tt>gem "mini_magick"</tt> to your Gemfile if you wish to use variants.
+#
+# Note that to create a variant it's necessary to download the entire blob file from the service and load it
+# into memory. The larger the image, the more memory is used. Because of this process, you also want to be
+# considerate about when the variant is actually processed. You shouldn't be processing variants inline in a
+# template, for example. Delay the processing to an on-demand controller, like the one provided in
+# ActiveStorage::VariantsController.
+#
+# To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided
+# by Active Storage like so:
+#
+# <%= image_tag Current.user.avatar.variant(resize: "100x100") %>
+#
+# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController
+# can then produce on-demand.
+#
+# When you do want to actually produce the variant needed, call +processed+. This will check that the variant
+# has already been processed and uploaded to the service, and, if so, just return that. Otherwise it will perform
+# the transformations, upload the variant to the service, and return itself again. Example:
+#
+# avatar.variant(resize: "100x100").processed.service_url
+#
+# This will create and process a variant of the avatar blob that's constrained to a height and width of 100.
+# Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
+#
+# A list of all possible transformations is available at https://www.imagemagick.org/script/mogrify.php. You can
+# combine as many as you like freely:
+#
+# avatar.variant(resize: "100x100", monochrome: true, flip: "-90")
+class ActiveStorage::Variant
+ include ActiveStorage::Downloading
+
+ WEB_IMAGE_CONTENT_TYPES = %w( image/png image/jpeg image/jpg image/gif )
+
+ attr_reader :blob, :variation
+ delegate :service, to: :blob
+
+ def initialize(blob, variation_or_variation_key)
+ @blob, @variation = blob, ActiveStorage::Variation.wrap(variation_or_variation_key)
+ end
+
+ # Returns the variant instance itself after it's been processed or an existing processing has been found on the service.
+ def processed
+ process unless processed?
+ self
+ end
+
+ # Returns a combination key of the blob and the variation that together identifies a specific variant.
+ def key
+ "variants/#{blob.key}/#{Digest::SHA256.hexdigest(variation.key)}"
+ end
+
+ # Returns the URL of the variant on the service. This URL is intended to be short-lived for security and not used directly
+ # with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
+ # Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
+ # it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
+ #
+ # Use <tt>url_for(variant)</tt> (or the implied form, like +link_to variant+ or +redirect_to variant+) to get the stable URL
+ # for a variant that points to the ActiveStorage::VariantsController, which in turn will use this +service_call+ method
+ # for its redirection.
+ def service_url(expires_in: service.url_expires_in, disposition: :inline)
+ service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
+ end
+
+ # Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be used interchangeably.
+ def image
+ self
+ end
+
+ private
+ def processed?
+ service.exist?(key)
+ end
+
+ def process
+ open_image do |image|
+ transform image
+ format image
+ upload image
+ end
+ end
+
+
+ def filename
+ if WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
+ blob.filename
+ else
+ ActiveStorage::Filename.new("#{blob.filename.base}.png")
+ end
+ end
+
+ def content_type
+ blob.content_type.presence_in(WEB_IMAGE_CONTENT_TYPES) || "image/png"
+ end
+
+
+ def open_image(&block)
+ image = download_image
+
+ begin
+ yield image
+ ensure
+ image.destroy!
+ end
+ end
+
+ def download_image
+ require "mini_magick"
+ MiniMagick::Image.create { |file| download_blob_to(file) }
+ end
+
+ def transform(image)
+ variation.transform(image)
+ end
+
+ def format(image)
+ image.format("PNG") unless WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
+ end
+
+ def upload(image)
+ File.open(image.path, "r") { |file| service.upload(key, file) }
+ end
+end
diff --git a/activestorage/app/models/active_storage/variation.rb b/activestorage/app/models/active_storage/variation.rb
new file mode 100644
index 0000000000..0046e6870b
--- /dev/null
+++ b/activestorage/app/models/active_storage/variation.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+# A set of transformations that can be applied to a blob to create a variant. This class is exposed via
+# the ActiveStorage::Blob#variant method and should rarely be used directly.
+#
+# In case you do need to use this directly, it's instantiated using a hash of transformations where
+# the key is the command and the value is the arguments. Example:
+#
+# ActiveStorage::Variation.new(resize: "100x100", monochrome: true, trim: true, rotate: "-90")
+#
+# A list of all possible transformations is available at https://www.imagemagick.org/script/mogrify.php.
+class ActiveStorage::Variation
+ attr_reader :transformations
+
+ class << self
+ # Returns a Variation instance based on the given variator. If the variator is a Variation, it is
+ # returned unmodified. If it is a String, it is passed to ActiveStorage::Variation.decode. Otherwise,
+ # it is assumed to be a transformations Hash and is passed directly to the constructor.
+ def wrap(variator)
+ case variator
+ when self
+ variator
+ when String
+ decode variator
+ else
+ new variator
+ end
+ end
+
+ # Returns a Variation instance with the transformations that were encoded by +encode+.
+ def decode(key)
+ new ActiveStorage.verifier.verify(key, purpose: :variation)
+ end
+
+ # Returns a signed key for the +transformations+, which can be used to refer to a specific
+ # variation in a URL or combined key (like <tt>ActiveStorage::Variant#key</tt>).
+ def encode(transformations)
+ ActiveStorage.verifier.generate(transformations, purpose: :variation)
+ end
+ end
+
+ def initialize(transformations)
+ @transformations = transformations
+ end
+
+ # Accepts an open MiniMagick image instance, like what's returned by <tt>MiniMagick::Image.read(io)</tt>,
+ # and performs the +transformations+ against it. The transformed image instance is then returned.
+ def transform(image)
+ transformations.each do |name, argument_or_subtransformations|
+ image.mogrify do |command|
+ if name.to_s == "combine_options"
+ argument_or_subtransformations.each do |subtransformation_name, subtransformation_argument|
+ pass_transform_argument(command, subtransformation_name, subtransformation_argument)
+ end
+ else
+ pass_transform_argument(command, name, argument_or_subtransformations)
+ end
+ end
+ end
+ end
+
+ # Returns a signed key for all the +transformations+ that this variation was instantiated with.
+ def key
+ self.class.encode(transformations)
+ end
+
+ private
+ def pass_transform_argument(command, method, argument)
+ if eligible_argument?(argument)
+ command.public_send(method, argument)
+ else
+ command.public_send(method)
+ end
+ end
+
+ def eligible_argument?(argument)
+ argument.present? && argument != true
+ end
+end
diff --git a/activestorage/bin/test b/activestorage/bin/test
new file mode 100755
index 0000000000..c53377cc97
--- /dev/null
+++ b/activestorage/bin/test
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+COMPONENT_ROOT = File.expand_path("..", __dir__)
+require_relative "../../tools/test"
diff --git a/activestorage/config/routes.rb b/activestorage/config/routes.rb
new file mode 100644
index 0000000000..3f4129d915
--- /dev/null
+++ b/activestorage/config/routes.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+Rails.application.routes.draw do
+ get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
+
+ direct :rails_blob do |blob, options|
+ route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
+ end
+
+ resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) }
+ resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
+
+
+ get "/rails/active_storage/variants/:signed_blob_id/:variation_key/*filename" => "active_storage/variants#show", as: :rails_blob_variation
+
+ direct :rails_variant do |variant, options|
+ signed_blob_id = variant.blob.signed_id
+ variation_key = variant.variation.key
+ filename = variant.blob.filename
+
+ route_for(:rails_blob_variation, signed_blob_id, variation_key, filename, options)
+ end
+
+ resolve("ActiveStorage::Variant") { |variant, options| route_for(:rails_variant, variant, options) }
+
+
+ get "/rails/active_storage/previews/:signed_blob_id/:variation_key/*filename" => "active_storage/previews#show", as: :rails_blob_preview
+
+ direct :rails_preview do |preview, options|
+ signed_blob_id = preview.blob.signed_id
+ variation_key = preview.variation.key
+ filename = preview.blob.filename
+
+ route_for(:rails_blob_preview, signed_blob_id, variation_key, filename, options)
+ end
+
+ resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_preview, preview, options) }
+
+
+ get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
+ put "/rails/active_storage/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
+ post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
+end
diff --git a/activestorage/db/migrate/20170806125915_create_active_storage_tables.rb b/activestorage/db/migrate/20170806125915_create_active_storage_tables.rb
new file mode 100644
index 0000000000..9e31e3966a
--- /dev/null
+++ b/activestorage/db/migrate/20170806125915_create_active_storage_tables.rb
@@ -0,0 +1,25 @@
+class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
+ def change
+ create_table :active_storage_blobs do |t|
+ t.string :key, null: false
+ t.string :filename, null: false
+ t.string :content_type
+ t.text :metadata
+ t.bigint :byte_size, null: false
+ t.string :checksum, null: false
+ t.datetime :created_at, null: false
+
+ t.index [ :key ], unique: true
+ end
+
+ create_table :active_storage_attachments do |t|
+ t.string :name, null: false
+ t.references :record, null: false, polymorphic: true, index: false
+ t.references :blob, null: false
+
+ t.datetime :created_at, null: false
+
+ t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage.rb b/activestorage/lib/active_storage.rb
new file mode 100644
index 0000000000..4e6c02f71b
--- /dev/null
+++ b/activestorage/lib/active_storage.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+#--
+# Copyright (c) 2017-2018 David Heinemeier Hansson, Basecamp
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#++
+
+require "active_record"
+require "active_support"
+require "active_support/rails"
+require "active_storage/version"
+
+module ActiveStorage
+ extend ActiveSupport::Autoload
+
+ autoload :Attached
+ autoload :Service
+ autoload :Previewer
+ autoload :Analyzer
+
+ mattr_accessor :logger
+ mattr_accessor :verifier
+ mattr_accessor :queue
+ mattr_accessor :previewers, default: []
+ mattr_accessor :analyzers, default: []
+ mattr_accessor :variable_content_types, default: []
+end
diff --git a/activestorage/lib/active_storage/analyzer.rb b/activestorage/lib/active_storage/analyzer.rb
new file mode 100644
index 0000000000..7c4168c1a0
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require "active_storage/downloading"
+
+module ActiveStorage
+ # This is an abstract base class for analyzers, which extract metadata from blobs. See
+ # ActiveStorage::Analyzer::ImageAnalyzer for an example of a concrete subclass.
+ class Analyzer
+ include Downloading
+
+ attr_reader :blob
+
+ # Implement this method in a concrete subclass. Have it return true when given a blob from which
+ # the analyzer can extract metadata.
+ def self.accept?(blob)
+ false
+ end
+
+ def initialize(blob)
+ @blob = blob
+ end
+
+ # Override this method in a concrete subclass. Have it return a Hash of metadata.
+ def metadata
+ raise NotImplementedError
+ end
+
+ private
+ def logger #:doc:
+ ActiveStorage.logger
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/analyzer/image_analyzer.rb b/activestorage/lib/active_storage/analyzer/image_analyzer.rb
new file mode 100644
index 0000000000..25e0251e6e
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer/image_analyzer.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ # Extracts width and height in pixels from an image blob.
+ #
+ # Example:
+ #
+ # ActiveStorage::Analyzer::ImageAnalyzer.new(blob).metadata
+ # # => { width: 4104, height: 2736 }
+ #
+ # This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires
+ # the {ImageMagick}[http://www.imagemagick.org] system library. These libraries are not provided by Rails; you must
+ # install them yourself to use this analyzer.
+ class Analyzer::ImageAnalyzer < Analyzer
+ def self.accept?(blob)
+ blob.image?
+ end
+
+ def metadata
+ read_image do |image|
+ { width: image.width, height: image.height }
+ end
+ rescue LoadError
+ logger.info "Skipping image analysis because the mini_magick gem isn't installed"
+ {}
+ end
+
+ private
+ def read_image
+ download_blob_to_tempfile do |file|
+ require "mini_magick"
+ yield MiniMagick::Image.new(file.path)
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/analyzer/null_analyzer.rb b/activestorage/lib/active_storage/analyzer/null_analyzer.rb
new file mode 100644
index 0000000000..8ff7ce48e5
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer/null_analyzer.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Analyzer::NullAnalyzer < Analyzer # :nodoc:
+ def self.accept?(blob)
+ true
+ end
+
+ def metadata
+ {}
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/analyzer/video_analyzer.rb b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
new file mode 100644
index 0000000000..1c144baa37
--- /dev/null
+++ b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/compact"
+
+module ActiveStorage
+ # Extracts the following from a video blob:
+ #
+ # * Width (pixels)
+ # * Height (pixels)
+ # * Duration (seconds)
+ # * Angle (degrees)
+ # * Aspect ratio
+ #
+ # Example:
+ #
+ # ActiveStorage::VideoAnalyzer.new(blob).metadata
+ # # => { width: 640, height: 480, duration: 5.0, angle: 0, aspect_ratio: [4, 3] }
+ #
+ # This analyzer requires the {ffmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails. You must
+ # install ffmpeg yourself to use this analyzer.
+ class Analyzer::VideoAnalyzer < Analyzer
+ class_attribute :ffprobe_path, default: "ffprobe"
+
+ def self.accept?(blob)
+ blob.video?
+ end
+
+ def metadata
+ { width: width, height: height, duration: duration, angle: angle, aspect_ratio: aspect_ratio }.compact
+ end
+
+ private
+ def width
+ rotated? ? raw_height : raw_width
+ end
+
+ def height
+ rotated? ? raw_width : raw_height
+ end
+
+ def raw_width
+ Integer(video_stream["width"]) if video_stream["width"]
+ end
+
+ def raw_height
+ Integer(video_stream["height"]) if video_stream["height"]
+ end
+
+ def duration
+ Float(video_stream["duration"]) if video_stream["duration"]
+ end
+
+ def angle
+ Integer(tags["rotate"]) if tags["rotate"]
+ end
+
+ def aspect_ratio
+ if descriptor = video_stream["display_aspect_ratio"]
+ descriptor.split(":", 2).collect(&:to_i)
+ end
+ end
+
+ def rotated?
+ angle == 90 || angle == 270
+ end
+
+
+ def tags
+ @tags ||= video_stream["tags"] || {}
+ end
+
+ def video_stream
+ @video_stream ||= streams.detect { |stream| stream["codec_type"] == "video" } || {}
+ end
+
+ def streams
+ probe["streams"] || []
+ end
+
+ def probe
+ download_blob_to_tempfile { |file| probe_from(file) }
+ end
+
+ def probe_from(file)
+ IO.popen([ ffprobe_path, "-print_format", "json", "-show_streams", "-v", "error", file.path ]) do |output|
+ JSON.parse(output.read)
+ end
+ rescue Errno::ENOENT
+ logger.info "Skipping video analysis because ffmpeg isn't installed"
+ {}
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/attached.rb b/activestorage/lib/active_storage/attached.rb
new file mode 100644
index 0000000000..c08fd56652
--- /dev/null
+++ b/activestorage/lib/active_storage/attached.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "action_dispatch"
+require "action_dispatch/http/upload"
+require "active_support/core_ext/module/delegation"
+
+module ActiveStorage
+ # Abstract base class for the concrete ActiveStorage::Attached::One and ActiveStorage::Attached::Many
+ # classes that both provide proxy access to the blob association for a record.
+ class Attached
+ attr_reader :name, :record, :dependent
+
+ def initialize(name, record, dependent:)
+ @name, @record, @dependent = name, record, dependent
+ end
+
+ private
+ def create_blob_from(attachable)
+ case attachable
+ when ActiveStorage::Blob
+ attachable
+ when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
+ ActiveStorage::Blob.create_after_upload! \
+ io: attachable.open,
+ filename: attachable.original_filename,
+ content_type: attachable.content_type
+ when Hash
+ ActiveStorage::Blob.create_after_upload!(attachable)
+ when String
+ ActiveStorage::Blob.find_signed(attachable)
+ else
+ nil
+ end
+ end
+ end
+end
+
+require "active_storage/attached/one"
+require "active_storage/attached/many"
+require "active_storage/attached/macros"
diff --git a/activestorage/lib/active_storage/attached/macros.rb b/activestorage/lib/active_storage/attached/macros.rb
new file mode 100644
index 0000000000..2b38a9b887
--- /dev/null
+++ b/activestorage/lib/active_storage/attached/macros.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ # Provides the class-level DSL for declaring that an Active Record model has attached blobs.
+ module Attached::Macros
+ # Specifies the relation between a single attachment and the model.
+ #
+ # class User < ActiveRecord::Base
+ # has_one_attached :avatar
+ # end
+ #
+ # There is no column defined on the model side, Active Storage takes
+ # care of the mapping between your records and the attachment.
+ #
+ # To avoid N+1 queries, you can include the attached blobs in your query like so:
+ #
+ # User.with_attached_avatar
+ #
+ # Under the covers, this relationship is implemented as a +has_one+ association to a
+ # ActiveStorage::Attachment record and a +has_one-through+ association to a
+ # ActiveStorage::Blob record. These associations are available as +avatar_attachment+
+ # and +avatar_blob+. But you shouldn't need to work with these associations directly in
+ # most circumstances.
+ #
+ # The system has been designed to having you go through the ActiveStorage::Attached::One
+ # proxy that provides the dynamic proxy to the associations and factory methods, like +attach+.
+ #
+ # If the +:dependent+ option isn't set, the attachment will be purged
+ # (i.e. destroyed) whenever the record is destroyed.
+ def has_one_attached(name, dependent: :purge_later)
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{name}
+ @active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self, dependent: #{dependent == :purge_later ? ":purge_later" : "false"})
+ end
+
+ def #{name}=(attachable)
+ #{name}.attach(attachable)
+ end
+ CODE
+
+ has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record
+ has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob
+
+ scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) }
+
+ if dependent == :purge_later
+ before_destroy { public_send(name).purge_later }
+ end
+ end
+
+ # Specifies the relation between multiple attachments and the model.
+ #
+ # class Gallery < ActiveRecord::Base
+ # has_many_attached :photos
+ # end
+ #
+ # There are no columns defined on the model side, Active Storage takes
+ # care of the mapping between your records and the attachments.
+ #
+ # To avoid N+1 queries, you can include the attached blobs in your query like so:
+ #
+ # Gallery.where(user: Current.user).with_attached_photos
+ #
+ # Under the covers, this relationship is implemented as a +has_many+ association to a
+ # ActiveStorage::Attachment record and a +has_many-through+ association to a
+ # ActiveStorage::Blob record. These associations are available as +photos_attachments+
+ # and +photos_blobs+. But you shouldn't need to work with these associations directly in
+ # most circumstances.
+ #
+ # The system has been designed to having you go through the ActiveStorage::Attached::Many
+ # proxy that provides the dynamic proxy to the associations and factory methods, like +#attach+.
+ #
+ # If the +:dependent+ option isn't set, all the attachments will be purged
+ # (i.e. destroyed) whenever the record is destroyed.
+ def has_many_attached(name, dependent: :purge_later)
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{name}
+ @active_storage_attached_#{name} ||= ActiveStorage::Attached::Many.new("#{name}", self, dependent: #{dependent == :purge_later ? ":purge_later" : "false"})
+ end
+
+ def #{name}=(attachables)
+ #{name}.attach(attachables)
+ end
+ CODE
+
+ has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment"
+ has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob
+
+ scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) }
+
+ if dependent == :purge_later
+ before_destroy { public_send(name).purge_later }
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/attached/many.rb b/activestorage/lib/active_storage/attached/many.rb
new file mode 100644
index 0000000000..6eace65b79
--- /dev/null
+++ b/activestorage/lib/active_storage/attached/many.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ # Decorated proxy object representing of multiple attachments to a model.
+ class Attached::Many < Attached
+ delegate_missing_to :attachments
+
+ # Returns all the associated attachment records.
+ #
+ # All methods called on this proxy object that aren't listed here will automatically be delegated to +attachments+.
+ def attachments
+ record.public_send("#{name}_attachments")
+ end
+
+ # Associates one or several attachments with the current record, saving them to the database.
+ #
+ # document.images.attach(params[:images]) # Array of ActionDispatch::Http::UploadedFile objects
+ # document.images.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
+ # document.images.attach(io: File.open("/path/to/racecar.jpg"), filename: "racecar.jpg", content_type: "image/jpg")
+ # document.images.attach([ first_blob, second_blob ])
+ def attach(*attachables)
+ attachables.flatten.collect do |attachable|
+ if record.new_record?
+ attachments.build(record: record, blob: create_blob_from(attachable))
+ else
+ attachments.create!(record: record, blob: create_blob_from(attachable))
+ end
+ end
+ end
+
+ # Returns true if any attachments has been made.
+ #
+ # class Gallery < ActiveRecord::Base
+ # has_many_attached :photos
+ # end
+ #
+ # Gallery.new.photos.attached? # => false
+ def attached?
+ attachments.any?
+ end
+
+ # Deletes associated attachments without purging them, leaving their respective blobs in place.
+ def detach
+ attachments.destroy_all if attached?
+ end
+
+ # Directly purges each associated attachment (i.e. destroys the blobs and
+ # attachments and deletes the files on the service).
+ def purge
+ if attached?
+ attachments.each(&:purge)
+ attachments.reload
+ end
+ end
+
+ # Purges each associated attachment through the queuing system.
+ def purge_later
+ if attached?
+ attachments.each(&:purge_later)
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/attached/one.rb b/activestorage/lib/active_storage/attached/one.rb
new file mode 100644
index 0000000000..0244232b2c
--- /dev/null
+++ b/activestorage/lib/active_storage/attached/one.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ # Representation of a single attachment to a model.
+ class Attached::One < Attached
+ delegate_missing_to :attachment
+
+ # Returns the associated attachment record.
+ #
+ # You don't have to call this method to access the attachment's methods as
+ # they are all available at the model level.
+ def attachment
+ record.public_send("#{name}_attachment")
+ end
+
+ # Associates a given attachment with the current record, saving it to the database.
+ #
+ # person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object
+ # person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
+ # person.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg")
+ # person.avatar.attach(avatar_blob) # ActiveStorage::Blob object
+ def attach(attachable)
+ if attached? && dependent == :purge_later
+ replace attachable
+ else
+ write_attachment build_attachment_from(attachable)
+ end
+ end
+
+ # Returns +true+ if an attachment has been made.
+ #
+ # class User < ActiveRecord::Base
+ # has_one_attached :avatar
+ # end
+ #
+ # User.new.avatar.attached? # => false
+ def attached?
+ attachment.present?
+ end
+
+ # Deletes the attachment without purging it, leaving its blob in place.
+ def detach
+ if attached?
+ attachment.destroy
+ write_attachment nil
+ end
+ end
+
+ # Directly purges the attachment (i.e. destroys the blob and
+ # attachment and deletes the file on the service).
+ def purge
+ if attached?
+ attachment.purge
+ write_attachment nil
+ end
+ end
+
+ # Purges the attachment through the queuing system.
+ def purge_later
+ if attached?
+ attachment.purge_later
+ end
+ end
+
+ private
+ def replace(attachable)
+ blob.tap do
+ transaction do
+ detach
+ write_attachment build_attachment_from(attachable)
+ end
+ end.purge_later
+ end
+
+ def build_attachment_from(attachable)
+ ActiveStorage::Attachment.new(record: record, name: name, blob: create_blob_from(attachable))
+ end
+
+ def write_attachment(attachment)
+ record.public_send("#{name}_attachment=", attachment)
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/downloading.rb b/activestorage/lib/active_storage/downloading.rb
new file mode 100644
index 0000000000..a57fda49b4
--- /dev/null
+++ b/activestorage/lib/active_storage/downloading.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ module Downloading
+ private
+ # Opens a new tempfile in #tempdir and copies blob data into it. Yields the tempfile.
+ def download_blob_to_tempfile # :doc:
+ Tempfile.open([ "ActiveStorage", blob.filename.extension_with_delimiter ], tempdir) do |file|
+ download_blob_to file
+ yield file
+ end
+ end
+
+ # Efficiently downloads blob data into the given file.
+ def download_blob_to(file) # :doc:
+ file.binmode
+ blob.download { |chunk| file.write(chunk) }
+ file.rewind
+ end
+
+ # Returns the directory in which tempfiles should be opened. Defaults to +Dir.tmpdir+.
+ def tempdir # :doc:
+ Dir.tmpdir
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb
new file mode 100644
index 0000000000..b41d8bb4d7
--- /dev/null
+++ b/activestorage/lib/active_storage/engine.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require "rails"
+require "active_storage"
+
+require "active_storage/previewer/pdf_previewer"
+require "active_storage/previewer/video_previewer"
+
+require "active_storage/analyzer/image_analyzer"
+require "active_storage/analyzer/video_analyzer"
+
+module ActiveStorage
+ class Engine < Rails::Engine # :nodoc:
+ isolate_namespace ActiveStorage
+
+ config.active_storage = ActiveSupport::OrderedOptions.new
+ config.active_storage.previewers = [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
+ config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ]
+ config.active_storage.variable_content_types = [ "image/png", "image/gif", "image/jpg", "image/jpeg", "image/vnd.adobe.photoshop" ]
+ config.active_storage.paths = ActiveSupport::OrderedOptions.new
+
+ config.eager_load_namespaces << ActiveStorage
+
+ initializer "active_storage.configs" do
+ config.after_initialize do |app|
+ ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
+ ActiveStorage.queue = app.config.active_storage.queue
+ ActiveStorage.previewers = app.config.active_storage.previewers || []
+ ActiveStorage.analyzers = app.config.active_storage.analyzers || []
+ ActiveStorage.variable_content_types = app.config.active_storage.variable_content_types || []
+ end
+ end
+
+ initializer "active_storage.attached" do
+ require "active_storage/attached"
+
+ ActiveSupport.on_load(:active_record) do
+ extend ActiveStorage::Attached::Macros
+ end
+ end
+
+ initializer "active_storage.verifier" do
+ config.after_initialize do |app|
+ ActiveStorage.verifier = app.message_verifier("ActiveStorage")
+ end
+ end
+
+ initializer "active_storage.services" do
+ config.to_prepare do
+ if config_choice = Rails.configuration.active_storage.service
+ configs = Rails.configuration.active_storage.service_configurations ||= begin
+ config_file = Pathname.new(Rails.root.join("config/storage.yml"))
+ raise("Couldn't find Active Storage configuration in #{config_file}") unless config_file.exist?
+
+ require "yaml"
+ require "erb"
+
+ YAML.load(ERB.new(config_file.read).result) || {}
+ rescue Psych::SyntaxError => e
+ raise "YAML syntax error occurred while parsing #{config_file}. " \
+ "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
+ "Error: #{e.message}"
+ end
+
+ ActiveStorage::Blob.service =
+ begin
+ ActiveStorage::Service.configure config_choice, configs
+ rescue => e
+ raise e, "Cannot load `Rails.config.active_storage.service`:\n#{e.message}", e.backtrace
+ end
+ end
+ end
+ end
+
+ initializer "active_storage.paths" do
+ config.after_initialize do |app|
+ if ffprobe_path = app.config.active_storage.paths.ffprobe
+ ActiveStorage::Analyzer::VideoAnalyzer.ffprobe_path = ffprobe_path
+ end
+
+ if ffmpeg_path = app.config.active_storage.paths.ffmpeg
+ ActiveStorage::Previewer::VideoPreviewer.ffmpeg_path = ffmpeg_path
+ end
+
+ if mutool_path = app.config.active_storage.paths.mutool
+ ActiveStorage::Previewer::PDFPreviewer.mutool_path = mutool_path
+ end
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/gem_version.rb b/activestorage/lib/active_storage/gem_version.rb
new file mode 100644
index 0000000000..f048bb0b77
--- /dev/null
+++ b/activestorage/lib/active_storage/gem_version.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ # Returns the version of the currently loaded Active Storage as a <tt>Gem::Version</tt>.
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 5
+ MINOR = 2
+ TINY = 0
+ PRE = "beta2"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/activestorage/lib/active_storage/log_subscriber.rb b/activestorage/lib/active_storage/log_subscriber.rb
new file mode 100644
index 0000000000..a4e148c1a5
--- /dev/null
+++ b/activestorage/lib/active_storage/log_subscriber.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require "active_support/log_subscriber"
+
+module ActiveStorage
+ class LogSubscriber < ActiveSupport::LogSubscriber
+ def service_upload(event)
+ message = "Uploaded file to key: #{key_in(event)}"
+ message += " (checksum: #{event.payload[:checksum]})" if event.payload[:checksum]
+ info event, color(message, GREEN)
+ end
+
+ def service_download(event)
+ info event, color("Downloaded file from key: #{key_in(event)}", BLUE)
+ end
+
+ def service_delete(event)
+ info event, color("Deleted file from key: #{key_in(event)}", RED)
+ end
+
+ def service_delete_prefixed(event)
+ info event, color("Deleted files by key prefix: #{event.payload[:prefix]}", RED)
+ end
+
+ def service_exist(event)
+ debug event, color("Checked if file exists at key: #{key_in(event)} (#{event.payload[:exist] ? "yes" : "no"})", BLUE)
+ end
+
+ def service_url(event)
+ debug event, color("Generated URL for file at key: #{key_in(event)} (#{event.payload[:url]})", BLUE)
+ end
+
+ def logger
+ ActiveStorage.logger
+ end
+
+ private
+ def info(event, colored_message)
+ super log_prefix_for_service(event) + colored_message
+ end
+
+ def debug(event, colored_message)
+ super log_prefix_for_service(event) + colored_message
+ end
+
+ def log_prefix_for_service(event)
+ color " #{event.payload[:service]} Storage (#{event.duration.round(1)}ms) ", CYAN
+ end
+
+ def key_in(event)
+ event.payload[:key]
+ end
+ end
+end
+
+ActiveStorage::LogSubscriber.attach_to :active_storage
diff --git a/activestorage/lib/active_storage/previewer.rb b/activestorage/lib/active_storage/previewer.rb
new file mode 100644
index 0000000000..7db9ae5956
--- /dev/null
+++ b/activestorage/lib/active_storage/previewer.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require "active_storage/downloading"
+
+module ActiveStorage
+ # This is an abstract base class for previewers, which generate images from blobs. See
+ # ActiveStorage::Previewer::PDFPreviewer and ActiveStorage::Previewer::VideoPreviewer for examples of
+ # concrete subclasses.
+ class Previewer
+ include Downloading
+
+ attr_reader :blob
+
+ # Implement this method in a concrete subclass. Have it return true when given a blob from which
+ # the previewer can generate an image.
+ def self.accept?(blob)
+ false
+ end
+
+ def initialize(blob)
+ @blob = blob
+ end
+
+ # Override this method in a concrete subclass. Have it yield an attachable preview image (i.e.
+ # anything accepted by ActiveStorage::Attached::One#attach).
+ def preview
+ raise NotImplementedError
+ end
+
+ private
+ # Executes a system command, capturing its binary output in a tempfile. Yields the tempfile.
+ #
+ # Use this method to shell out to a system library (e.g. mupdf or ffmpeg) for preview image
+ # generation. The resulting tempfile can be used as the +:io+ value in an attachable Hash:
+ #
+ # def preview
+ # download_blob_to_tempfile do |input|
+ # draw "my-drawing-command", input.path, "--format", "png", "-" do |output|
+ # yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
+ # end
+ # end
+ # end
+ #
+ # The output tempfile is opened in the directory returned by ActiveStorage::Downloading#tempdir.
+ def draw(*argv) #:doc:
+ Tempfile.open("ActiveStorage", tempdir) do |file|
+ capture(*argv, to: file)
+ yield file
+ end
+ end
+
+ def capture(*argv, to:)
+ to.binmode
+ IO.popen(argv, err: File::NULL) { |out| IO.copy_stream(out, to) }
+ to.rewind
+ end
+
+ def logger #:doc:
+ ActiveStorage.logger
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/previewer/pdf_previewer.rb b/activestorage/lib/active_storage/previewer/pdf_previewer.rb
new file mode 100644
index 0000000000..b84aefcc9c
--- /dev/null
+++ b/activestorage/lib/active_storage/previewer/pdf_previewer.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Previewer::PDFPreviewer < Previewer
+ class_attribute :mutool_path, default: "mutool"
+
+ def self.accept?(blob)
+ blob.content_type == "application/pdf"
+ end
+
+ def preview
+ download_blob_to_tempfile do |input|
+ draw_first_page_from input do |output|
+ yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
+ end
+ end
+ end
+
+ private
+ def draw_first_page_from(file, &block)
+ draw mutool_path, "draw", "-F", "png", "-o", "-", file.path, "1", &block
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/previewer/video_previewer.rb b/activestorage/lib/active_storage/previewer/video_previewer.rb
new file mode 100644
index 0000000000..5d06e33f44
--- /dev/null
+++ b/activestorage/lib/active_storage/previewer/video_previewer.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Previewer::VideoPreviewer < Previewer
+ class_attribute :ffmpeg_path, default: "ffmpeg"
+
+ def self.accept?(blob)
+ blob.video?
+ end
+
+ def preview
+ download_blob_to_tempfile do |input|
+ draw_relevant_frame_from input do |output|
+ yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
+ end
+ end
+ end
+
+ private
+ def draw_relevant_frame_from(file, &block)
+ draw ffmpeg_path, "-i", file.path, "-y", "-vcodec", "png",
+ "-vf", "thumbnail", "-vframes", "1", "-f", "image2", "-", &block
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/service.rb b/activestorage/lib/active_storage/service.rb
new file mode 100644
index 0000000000..f2e1269f27
--- /dev/null
+++ b/activestorage/lib/active_storage/service.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+require "active_storage/log_subscriber"
+
+module ActiveStorage
+ class IntegrityError < StandardError; end
+
+ # Abstract class serving as an interface for concrete services.
+ #
+ # The available services are:
+ #
+ # * +Disk+, to manage attachments saved directly on the hard drive.
+ # * +GCS+, to manage attachments through Google Cloud Storage.
+ # * +S3+, to manage attachments through Amazon S3.
+ # * +AzureStorage+, to manage attachments through Microsoft Azure Storage.
+ # * +Mirror+, to be able to use several services to manage attachments.
+ #
+ # Inside a Rails application, you can set-up your services through the
+ # generated <tt>config/storage.yml</tt> file and reference one
+ # of the aforementioned constant under the +service+ key. For example:
+ #
+ # local:
+ # service: Disk
+ # root: <%= Rails.root.join("storage") %>
+ #
+ # You can checkout the service's constructor to know which keys are required.
+ #
+ # Then, in your application's configuration, you can specify the service to
+ # use like this:
+ #
+ # config.active_storage.service = :local
+ #
+ # If you are using Active Storage outside of a Ruby on Rails application, you
+ # can configure the service to use like this:
+ #
+ # ActiveStorage::Blob.service = ActiveStorage::Service.configure(
+ # :Disk,
+ # root: Pathname("/foo/bar/storage")
+ # )
+ class Service
+ extend ActiveSupport::Autoload
+ autoload :Configurator
+
+ class_attribute :url_expires_in, default: 5.minutes
+
+ class << self
+ # Configure an Active Storage service by name from a set of configurations,
+ # typically loaded from a YAML file. The Active Storage engine uses this
+ # to set the global Active Storage service when the app boots.
+ def configure(service_name, configurations)
+ Configurator.build(service_name, configurations)
+ end
+
+ # Override in subclasses that stitch together multiple services and hence
+ # need to build additional services using the configurator.
+ #
+ # Passes the configurator and all of the service's config as keyword args.
+ #
+ # See MirrorService for an example.
+ def build(configurator:, service: nil, **service_config) #:nodoc:
+ new(**service_config)
+ end
+ end
+
+ # Upload the +io+ to the +key+ specified. If a +checksum+ is provided, the service will
+ # ensure a match when the upload has completed or raise an ActiveStorage::IntegrityError.
+ def upload(key, io, checksum: nil)
+ raise NotImplementedError
+ end
+
+ # Return the content of the file at the +key+.
+ def download(key)
+ raise NotImplementedError
+ end
+
+ # Delete the file at the +key+.
+ def delete(key)
+ raise NotImplementedError
+ end
+
+ # Delete files at keys starting with the +prefix+.
+ def delete_prefixed(prefix)
+ raise NotImplementedError
+ end
+
+ # Return +true+ if a file exists at the +key+.
+ def exist?(key)
+ raise NotImplementedError
+ end
+
+ # Returns a signed, temporary URL for the file at the +key+. The URL will be valid for the amount
+ # of seconds specified in +expires_in+. You most also provide the +disposition+ (+:inline+ or +:attachment+),
+ # +filename+, and +content_type+ that you wish the file to be served with on request.
+ def url(key, expires_in:, disposition:, filename:, content_type:)
+ raise NotImplementedError
+ end
+
+ # Returns a signed, temporary URL that a direct upload file can be PUT to on the +key+.
+ # The URL will be valid for the amount of seconds specified in +expires_in+.
+ # You must also provide the +content_type+, +content_length+, and +checksum+ of the file
+ # that will be uploaded. All these attributes will be validated by the service upon upload.
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
+ raise NotImplementedError
+ end
+
+ # Returns a Hash of headers for +url_for_direct_upload+ requests.
+ def headers_for_direct_upload(key, filename:, content_type:, content_length:, checksum:)
+ {}
+ end
+
+ private
+ def instrument(operation, payload = {}, &block)
+ ActiveSupport::Notifications.instrument(
+ "service_#{operation}.active_storage",
+ payload.merge(service: service_name), &block)
+ end
+
+ def service_name
+ # ActiveStorage::Service::DiskService => Disk
+ self.class.name.split("::").third.remove("Service")
+ end
+
+ def content_disposition_with(type: "inline", filename:)
+ (type.to_s.presence_in(%w( attachment inline )) || "inline") + "; #{filename.parameters}"
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/service/azure_storage_service.rb b/activestorage/lib/active_storage/service/azure_storage_service.rb
new file mode 100644
index 0000000000..0a9eb7f23d
--- /dev/null
+++ b/activestorage/lib/active_storage/service/azure_storage_service.rb
@@ -0,0 +1,140 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/numeric/bytes"
+require "azure/storage"
+require "azure/storage/core/auth/shared_access_signature"
+
+module ActiveStorage
+ # Wraps the Microsoft Azure Storage Blob Service as an Active Storage service.
+ # See ActiveStorage::Service for the generic API documentation that applies to all services.
+ class Service::AzureStorageService < Service
+ attr_reader :client, :path, :blobs, :container, :signer
+
+ def initialize(path:, storage_account_name:, storage_access_key:, container:)
+ @client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key)
+ @signer = Azure::Storage::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
+ @blobs = client.blob_client
+ @container = container
+ @path = path
+ end
+
+ def upload(key, io, checksum: nil)
+ instrument :upload, key: key, checksum: checksum do
+ begin
+ blobs.create_block_blob(container, key, io, content_md5: checksum)
+ rescue Azure::Core::Http::HTTPError
+ raise ActiveStorage::IntegrityError
+ end
+ end
+ end
+
+ def download(key, &block)
+ if block_given?
+ instrument :streaming_download, key: key do
+ stream(key, &block)
+ end
+ else
+ instrument :download, key: key do
+ _, io = blobs.get_blob(container, key)
+ io.force_encoding(Encoding::BINARY)
+ end
+ end
+ end
+
+ def delete(key)
+ instrument :delete, key: key do
+ begin
+ blobs.delete_blob(container, key)
+ rescue Azure::Core::Http::HTTPError
+ # Ignore files already deleted
+ end
+ end
+ end
+
+ def delete_prefixed(prefix)
+ instrument :delete_prefixed, prefix: prefix do
+ marker = nil
+
+ loop do
+ results = blobs.list_blobs(container, prefix: prefix, marker: marker)
+
+ results.each do |blob|
+ blobs.delete_blob(container, blob.name)
+ end
+
+ break unless marker = results.continuation_token.presence
+ end
+ end
+ end
+
+ def exist?(key)
+ instrument :exist, key: key do |payload|
+ answer = blob_for(key).present?
+ payload[:exist] = answer
+ answer
+ end
+ end
+
+ def url(key, expires_in:, filename:, disposition:, content_type:)
+ instrument :url, key: key do |payload|
+ base_url = url_for(key)
+ generated_url = signer.signed_uri(
+ URI(base_url), false,
+ permissions: "r",
+ expiry: format_expiry(expires_in),
+ content_disposition: content_disposition_with(type: disposition, filename: filename),
+ content_type: content_type
+ ).to_s
+
+ payload[:url] = generated_url
+
+ generated_url
+ end
+ end
+
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
+ instrument :url, key: key do |payload|
+ base_url = url_for(key)
+ generated_url = signer.signed_uri(URI(base_url), false, permissions: "rw",
+ expiry: format_expiry(expires_in)).to_s
+
+ payload[:url] = generated_url
+
+ generated_url
+ end
+ end
+
+ def headers_for_direct_upload(key, content_type:, checksum:, **)
+ { "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-type" => "BlockBlob" }
+ end
+
+ private
+ def url_for(key)
+ "#{path}/#{container}/#{key}"
+ end
+
+ def blob_for(key)
+ blobs.get_blob_properties(container, key)
+ rescue Azure::Core::Http::HTTPError
+ false
+ end
+
+ def format_expiry(expires_in)
+ expires_in ? Time.now.utc.advance(seconds: expires_in).iso8601 : nil
+ end
+
+ # Reads the object for the given key in chunks, yielding each to the block.
+ def stream(key)
+ blob = blob_for(key)
+
+ chunk_size = 5.megabytes
+ offset = 0
+
+ while offset < blob.properties[:content_length]
+ _, chunk = blobs.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1)
+ yield chunk.force_encoding(Encoding::BINARY)
+ offset += chunk_size
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/service/configurator.rb b/activestorage/lib/active_storage/service/configurator.rb
new file mode 100644
index 0000000000..39951fd026
--- /dev/null
+++ b/activestorage/lib/active_storage/service/configurator.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Service::Configurator #:nodoc:
+ attr_reader :configurations
+
+ def self.build(service_name, configurations)
+ new(configurations).build(service_name)
+ end
+
+ def initialize(configurations)
+ @configurations = configurations.deep_symbolize_keys
+ end
+
+ def build(service_name)
+ config = config_for(service_name.to_sym)
+ resolve(config.fetch(:service)).build(**config, configurator: self)
+ end
+
+ private
+ def config_for(name)
+ configurations.fetch name do
+ raise "Missing configuration for the #{name.inspect} Active Storage service. Configurations available for #{configurations.keys.inspect}"
+ end
+ end
+
+ def resolve(class_name)
+ require "active_storage/service/#{class_name.to_s.underscore}_service"
+ ActiveStorage::Service.const_get(:"#{class_name}Service")
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb
new file mode 100644
index 0000000000..a8728c5bc3
--- /dev/null
+++ b/activestorage/lib/active_storage/service/disk_service.rb
@@ -0,0 +1,137 @@
+# frozen_string_literal: true
+
+require "fileutils"
+require "pathname"
+require "digest/md5"
+require "active_support/core_ext/numeric/bytes"
+
+module ActiveStorage
+ # Wraps a local disk path as an Active Storage service. See ActiveStorage::Service for the generic API
+ # documentation that applies to all services.
+ class Service::DiskService < Service
+ attr_reader :root
+
+ def initialize(root:)
+ @root = root
+ end
+
+ def upload(key, io, checksum: nil)
+ instrument :upload, key: key, checksum: checksum do
+ IO.copy_stream(io, make_path_for(key))
+ ensure_integrity_of(key, checksum) if checksum
+ end
+ end
+
+ def download(key)
+ if block_given?
+ instrument :streaming_download, key: key do
+ File.open(path_for(key), "rb") do |file|
+ while data = file.read(64.kilobytes)
+ yield data
+ end
+ end
+ end
+ else
+ instrument :download, key: key do
+ File.binread path_for(key)
+ end
+ end
+ end
+
+ def delete(key)
+ instrument :delete, key: key do
+ begin
+ File.delete path_for(key)
+ rescue Errno::ENOENT
+ # Ignore files already deleted
+ end
+ end
+ end
+
+ def delete_prefixed(prefix)
+ instrument :delete_prefixed, prefix: prefix do
+ Dir.glob(path_for("#{prefix}*")).each do |path|
+ FileUtils.rm_rf(path)
+ end
+ end
+ end
+
+ def exist?(key)
+ instrument :exist, key: key do |payload|
+ answer = File.exist? path_for(key)
+ payload[:exist] = answer
+ answer
+ end
+ end
+
+ def url(key, expires_in:, filename:, disposition:, content_type:)
+ instrument :url, key: key do |payload|
+ verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key)
+
+ generated_url =
+ if defined?(Rails.application)
+ Rails.application.routes.url_helpers.rails_disk_service_path \
+ verified_key_with_expiration,
+ filename: filename, disposition: content_disposition_with(type: disposition, filename: filename), content_type: content_type
+ else
+ "/rails/active_storage/disk/#{verified_key_with_expiration}/#{filename}?content_type=#{content_type}" \
+ "&disposition=#{content_disposition_with(type: disposition, filename: filename)}"
+ end
+
+ payload[:url] = generated_url
+
+ generated_url
+ end
+ end
+
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
+ instrument :url, key: key do |payload|
+ verified_token_with_expiration = ActiveStorage.verifier.generate(
+ {
+ key: key,
+ content_type: content_type,
+ content_length: content_length,
+ checksum: checksum
+ },
+ { expires_in: expires_in,
+ purpose: :blob_token }
+ )
+
+ generated_url =
+ if defined?(Rails.application)
+ Rails.application.routes.url_helpers.update_rails_disk_service_path verified_token_with_expiration
+ else
+ "/rails/active_storage/disk/#{verified_token_with_expiration}"
+ end
+
+ payload[:url] = generated_url
+
+ generated_url
+ end
+ end
+
+ def headers_for_direct_upload(key, content_type:, **)
+ { "Content-Type" => content_type }
+ end
+
+ private
+ def path_for(key)
+ File.join root, folder_for(key), key
+ end
+
+ def folder_for(key)
+ [ key[0..1], key[2..3] ].join("/")
+ end
+
+ def make_path_for(key)
+ path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) }
+ end
+
+ def ensure_integrity_of(key, checksum)
+ unless Digest::MD5.file(path_for(key)).base64digest == checksum
+ delete key
+ raise ActiveStorage::IntegrityError
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/service/gcs_service.rb b/activestorage/lib/active_storage/service/gcs_service.rb
new file mode 100644
index 0000000000..6f6f4105fe
--- /dev/null
+++ b/activestorage/lib/active_storage/service/gcs_service.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+gem "google-cloud-storage", "~> 1.8"
+
+require "google/cloud/storage"
+require "active_support/core_ext/object/to_query"
+
+module ActiveStorage
+ # Wraps the Google Cloud Storage as an Active Storage service. See ActiveStorage::Service for the generic API
+ # documentation that applies to all services.
+ class Service::GCSService < Service
+ def initialize(**config)
+ @config = config
+ end
+
+ def upload(key, io, checksum: nil)
+ instrument :upload, key: key, checksum: checksum do
+ begin
+ # The official GCS client library doesn't allow us to create a file with no Content-Type metadata.
+ # We need the file we create to have no Content-Type so we can control it via the response-content-type
+ # param in signed URLs. Workaround: let the GCS client create the file with an inferred
+ # Content-Type (usually "application/octet-stream") then clear it.
+ bucket.create_file(io, key, md5: checksum).update do |file|
+ file.content_type = nil
+ end
+ rescue Google::Cloud::InvalidArgumentError
+ raise ActiveStorage::IntegrityError
+ end
+ end
+ end
+
+ # FIXME: Download in chunks when given a block.
+ def download(key)
+ instrument :download, key: key do
+ io = file_for(key).download
+ io.rewind
+
+ if block_given?
+ yield io.read
+ else
+ io.read
+ end
+ end
+ end
+
+ def delete(key)
+ instrument :delete, key: key do
+ begin
+ file_for(key).delete
+ rescue Google::Cloud::NotFoundError
+ # Ignore files already deleted
+ end
+ end
+ end
+
+ def delete_prefixed(prefix)
+ instrument :delete_prefixed, prefix: prefix do
+ bucket.files(prefix: prefix).all(&:delete)
+ end
+ end
+
+ def exist?(key)
+ instrument :exist, key: key do |payload|
+ answer = file_for(key).exists?
+ payload[:exist] = answer
+ answer
+ end
+ end
+
+ def url(key, expires_in:, filename:, content_type:, disposition:)
+ instrument :url, key: key do |payload|
+ generated_url = file_for(key).signed_url expires: expires_in, query: {
+ "response-content-disposition" => content_disposition_with(type: disposition, filename: filename),
+ "response-content-type" => content_type
+ }
+
+ payload[:url] = generated_url
+
+ generated_url
+ end
+ end
+
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
+ instrument :url, key: key do |payload|
+ generated_url = bucket.signed_url key, method: "PUT", expires: expires_in,
+ content_type: content_type, content_md5: checksum
+
+ payload[:url] = generated_url
+
+ generated_url
+ end
+ end
+
+ def headers_for_direct_upload(key, content_type:, checksum:, **)
+ { "Content-Type" => content_type, "Content-MD5" => checksum }
+ end
+
+ private
+ attr_reader :config
+
+ def file_for(key)
+ bucket.file(key, skip_lookup: true)
+ end
+
+ def bucket
+ @bucket ||= client.bucket(config.fetch(:bucket))
+ end
+
+ def client
+ @client ||= Google::Cloud::Storage.new(config.except(:bucket))
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/service/mirror_service.rb b/activestorage/lib/active_storage/service/mirror_service.rb
new file mode 100644
index 0000000000..7eca8ce7f4
--- /dev/null
+++ b/activestorage/lib/active_storage/service/mirror_service.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/delegation"
+
+module ActiveStorage
+ # Wraps a set of mirror services and provides a single ActiveStorage::Service object that will all
+ # have the files uploaded to them. A +primary+ service is designated to answer calls to +download+, +exists?+,
+ # and +url+.
+ class Service::MirrorService < Service
+ attr_reader :primary, :mirrors
+
+ delegate :download, :exist?, :url, to: :primary
+
+ # Stitch together from named services.
+ def self.build(primary:, mirrors:, configurator:, **options) #:nodoc:
+ new \
+ primary: configurator.build(primary),
+ mirrors: mirrors.collect { |name| configurator.build name }
+ end
+
+ def initialize(primary:, mirrors:)
+ @primary, @mirrors = primary, mirrors
+ end
+
+ # Upload the +io+ to the +key+ specified to all services. If a +checksum+ is provided, all services will
+ # ensure a match when the upload has completed or raise an ActiveStorage::IntegrityError.
+ def upload(key, io, checksum: nil)
+ each_service.collect do |service|
+ service.upload key, io.tap(&:rewind), checksum: checksum
+ end
+ end
+
+ # Delete the file at the +key+ on all services.
+ def delete(key)
+ perform_across_services :delete, key
+ end
+
+ # Delete files at keys starting with the +prefix+ on all services.
+ def delete_prefixed(prefix)
+ perform_across_services :delete_prefixed, prefix
+ end
+
+ private
+ def each_service(&block)
+ [ primary, *mirrors ].each(&block)
+ end
+
+ def perform_across_services(method, *args)
+ # FIXME: Convert to be threaded
+ each_service.collect do |service|
+ service.public_send method, *args
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/service/s3_service.rb b/activestorage/lib/active_storage/service/s3_service.rb
new file mode 100644
index 0000000000..c95672f338
--- /dev/null
+++ b/activestorage/lib/active_storage/service/s3_service.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require "aws-sdk-s3"
+require "active_support/core_ext/numeric/bytes"
+
+module ActiveStorage
+ # Wraps the Amazon Simple Storage Service (S3) as an Active Storage service.
+ # See ActiveStorage::Service for the generic API documentation that applies to all services.
+ class Service::S3Service < Service
+ attr_reader :client, :bucket, :upload_options
+
+ def initialize(access_key_id:, secret_access_key:, region:, bucket:, upload: {}, **options)
+ @client = Aws::S3::Resource.new(access_key_id: access_key_id, secret_access_key: secret_access_key, region: region, **options)
+ @bucket = @client.bucket(bucket)
+
+ @upload_options = upload
+ end
+
+ def upload(key, io, checksum: nil)
+ instrument :upload, key: key, checksum: checksum do
+ begin
+ object_for(key).put(upload_options.merge(body: io, content_md5: checksum))
+ rescue Aws::S3::Errors::BadDigest
+ raise ActiveStorage::IntegrityError
+ end
+ end
+ end
+
+ def download(key, &block)
+ if block_given?
+ instrument :streaming_download, key: key do
+ stream(key, &block)
+ end
+ else
+ instrument :download, key: key do
+ object_for(key).get.body.read.force_encoding(Encoding::BINARY)
+ end
+ end
+ end
+
+ def delete(key)
+ instrument :delete, key: key do
+ object_for(key).delete
+ end
+ end
+
+ def delete_prefixed(prefix)
+ instrument :delete_prefixed, prefix: prefix do
+ bucket.objects(prefix: prefix).batch_delete!
+ end
+ end
+
+ def exist?(key)
+ instrument :exist, key: key do |payload|
+ answer = object_for(key).exists?
+ payload[:exist] = answer
+ answer
+ end
+ end
+
+ def url(key, expires_in:, filename:, disposition:, content_type:)
+ instrument :url, key: key do |payload|
+ generated_url = object_for(key).presigned_url :get, expires_in: expires_in.to_i,
+ response_content_disposition: content_disposition_with(type: disposition, filename: filename),
+ response_content_type: content_type
+
+ payload[:url] = generated_url
+
+ generated_url
+ end
+ end
+
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
+ instrument :url, key: key do |payload|
+ generated_url = object_for(key).presigned_url :put, expires_in: expires_in.to_i,
+ content_type: content_type, content_length: content_length, content_md5: checksum
+
+ payload[:url] = generated_url
+
+ generated_url
+ end
+ end
+
+ def headers_for_direct_upload(key, content_type:, checksum:, **)
+ { "Content-Type" => content_type, "Content-MD5" => checksum }
+ end
+
+ private
+ def object_for(key)
+ bucket.object(key)
+ end
+
+ # Reads the object for the given key in chunks, yielding each to the block.
+ def stream(key)
+ object = object_for(key)
+
+ chunk_size = 5.megabytes
+ offset = 0
+
+ while offset < object.content_length
+ yield object.get(range: "bytes=#{offset}-#{offset + chunk_size - 1}").body.read.force_encoding(Encoding::BINARY)
+ offset += chunk_size
+ end
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/version.rb b/activestorage/lib/active_storage/version.rb
new file mode 100644
index 0000000000..4b6631832b
--- /dev/null
+++ b/activestorage/lib/active_storage/version.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require_relative "gem_version"
+
+module ActiveStorage
+ # Returns the version of the currently loaded ActiveStorage as a <tt>Gem::Version</tt>
+ def self.version
+ gem_version
+ end
+end
diff --git a/activestorage/lib/tasks/activestorage.rake b/activestorage/lib/tasks/activestorage.rake
new file mode 100644
index 0000000000..296e91afa1
--- /dev/null
+++ b/activestorage/lib/tasks/activestorage.rake
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+namespace :active_storage do
+ desc "Copy over the migration needed to the application"
+ task install: :environment do
+ if Rake::Task.task_defined?("active_storage:install:migrations")
+ Rake::Task["active_storage:install:migrations"].invoke
+ else
+ Rake::Task["app:active_storage:install:migrations"].invoke
+ end
+ end
+end
diff --git a/activestorage/package.json b/activestorage/package.json
new file mode 100644
index 0000000000..621706000b
--- /dev/null
+++ b/activestorage/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "activestorage",
+ "version": "5.2.0-beta2",
+ "description": "Attach cloud and local files in Rails applications",
+ "main": "app/assets/javascripts/activestorage.js",
+ "files": [
+ "app/assets/javascripts/*.js"
+ ],
+ "homepage": "http://rubyonrails.org/",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/rails/rails.git"
+ },
+ "bugs": {
+ "url": "https://github.com/rails/rails/issues"
+ },
+ "author": "Javan Makhmali <javan@javan.us>",
+ "license": "MIT",
+ "devDependencies": {
+ "babel-core": "^6.25.0",
+ "babel-loader": "^7.1.1",
+ "babel-preset-env": "^1.6.0",
+ "eslint": "^4.3.0",
+ "eslint-plugin-import": "^2.7.0",
+ "spark-md5": "^3.0.0",
+ "webpack": "^3.4.0"
+ },
+ "scripts": {
+ "prebuild": "yarn lint",
+ "build": "webpack -p",
+ "lint": "eslint app/javascript"
+ }
+}
diff --git a/activestorage/test/analyzer/image_analyzer_test.rb b/activestorage/test/analyzer/image_analyzer_test.rb
new file mode 100644
index 0000000000..0d9f24c5c1
--- /dev/null
+++ b/activestorage/test/analyzer/image_analyzer_test.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+require "active_storage/analyzer/image_analyzer"
+
+class ActiveStorage::Analyzer::ImageAnalyzerTest < ActiveSupport::TestCase
+ test "analyzing a JPEG image" do
+ blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg")
+ metadata = blob.tap(&:analyze).metadata
+
+ assert_equal 4104, metadata[:width]
+ assert_equal 2736, metadata[:height]
+ end
+
+ test "analyzing an SVG image without an XML declaration" do
+ blob = create_file_blob(filename: "icon.svg", content_type: "image/svg+xml")
+ metadata = blob.tap(&:analyze).metadata
+
+ assert_equal 792, metadata[:width]
+ assert_equal 584, metadata[:height]
+ end
+end
diff --git a/activestorage/test/analyzer/video_analyzer_test.rb b/activestorage/test/analyzer/video_analyzer_test.rb
new file mode 100644
index 0000000000..b3b9c97fe4
--- /dev/null
+++ b/activestorage/test/analyzer/video_analyzer_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+require "active_storage/analyzer/video_analyzer"
+
+class ActiveStorage::Analyzer::VideoAnalyzerTest < ActiveSupport::TestCase
+ test "analyzing a video" do
+ blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4")
+ metadata = blob.tap(&:analyze).metadata
+
+ assert_equal 640, metadata[:width]
+ assert_equal 480, metadata[:height]
+ assert_equal [4, 3], metadata[:aspect_ratio]
+ assert_equal 5.166648, metadata[:duration]
+ assert_not_includes metadata, :angle
+ end
+
+ test "analyzing a rotated video" do
+ blob = create_file_blob(filename: "rotated_video.mp4", content_type: "video/mp4")
+ metadata = blob.tap(&:analyze).metadata
+
+ assert_equal 480, metadata[:width]
+ assert_equal 640, metadata[:height]
+ assert_equal [4, 3], metadata[:aspect_ratio]
+ assert_equal 5.227975, metadata[:duration]
+ assert_equal 90, metadata[:angle]
+ end
+
+ test "analyzing a video without a video stream" do
+ blob = create_file_blob(filename: "video_without_video_stream.mp4", content_type: "video/mp4")
+ assert_equal({ "analyzed" => true }, blob.tap(&:analyze).metadata)
+ end
+end
diff --git a/activestorage/test/controllers/blobs_controller_test.rb b/activestorage/test/controllers/blobs_controller_test.rb
new file mode 100644
index 0000000000..9c811df895
--- /dev/null
+++ b/activestorage/test/controllers/blobs_controller_test.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::BlobsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @blob = create_file_blob filename: "racecar.jpg"
+ end
+
+ test "showing blob with invalid signed ID" do
+ get rails_service_blob_url("invalid", "racecar.jpg")
+ assert_response :not_found
+ end
+
+ test "showing blob utilizes browser caching" do
+ get rails_blob_url(@blob)
+
+ assert_redirected_to(/racecar\.jpg/)
+ assert_equal "max-age=300, private", @response.headers["Cache-Control"]
+ end
+end
diff --git a/activestorage/test/controllers/direct_uploads_controller_test.rb b/activestorage/test/controllers/direct_uploads_controller_test.rb
new file mode 100644
index 0000000000..888767086c
--- /dev/null
+++ b/activestorage/test/controllers/direct_uploads_controller_test.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].present?
+ class ActiveStorage::S3DirectUploadsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @old_service = ActiveStorage::Blob.service
+ ActiveStorage::Blob.service = ActiveStorage::Service.configure(:s3, SERVICE_CONFIGURATIONS)
+ end
+
+ teardown do
+ ActiveStorage::Blob.service = @old_service
+ end
+
+ test "creating new direct upload" do
+ checksum = Digest::MD5.base64digest("Hello")
+
+ post rails_direct_uploads_url, params: { blob: {
+ filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain" } }
+
+ response.parsed_body.tap do |details|
+ assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed(details["signed_id"])
+ assert_equal "hello.txt", details["filename"]
+ assert_equal 6, details["byte_size"]
+ assert_equal checksum, details["checksum"]
+ assert_equal "text/plain", details["content_type"]
+ assert_match SERVICE_CONFIGURATIONS[:s3][:bucket], details["direct_upload"]["url"]
+ assert_match(/s3\.(\S+)?amazonaws\.com/, details["direct_upload"]["url"])
+ assert_equal({ "Content-Type" => "text/plain", "Content-MD5" => checksum }, details["direct_upload"]["headers"])
+ end
+ end
+ end
+else
+ puts "Skipping S3 Direct Upload tests because no S3 configuration was supplied"
+end
+
+if SERVICE_CONFIGURATIONS[:gcs]
+ class ActiveStorage::GCSDirectUploadsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @config = SERVICE_CONFIGURATIONS[:gcs]
+
+ @old_service = ActiveStorage::Blob.service
+ ActiveStorage::Blob.service = ActiveStorage::Service.configure(:gcs, SERVICE_CONFIGURATIONS)
+ end
+
+ teardown do
+ ActiveStorage::Blob.service = @old_service
+ end
+
+ test "creating new direct upload" do
+ checksum = Digest::MD5.base64digest("Hello")
+
+ post rails_direct_uploads_url, params: { blob: {
+ filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain" } }
+
+ @response.parsed_body.tap do |details|
+ assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed(details["signed_id"])
+ assert_equal "hello.txt", details["filename"]
+ assert_equal 6, details["byte_size"]
+ assert_equal checksum, details["checksum"]
+ assert_equal "text/plain", details["content_type"]
+ assert_match %r{storage\.googleapis\.com/#{@config[:bucket]}}, details["direct_upload"]["url"]
+ assert_equal({ "Content-Type" => "text/plain", "Content-MD5" => checksum }, details["direct_upload"]["headers"])
+ end
+ end
+ end
+else
+ puts "Skipping GCS Direct Upload tests because no GCS configuration was supplied"
+end
+
+if SERVICE_CONFIGURATIONS[:azure]
+ class ActiveStorage::AzureStorageDirectUploadsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @config = SERVICE_CONFIGURATIONS[:azure]
+
+ @old_service = ActiveStorage::Blob.service
+ ActiveStorage::Blob.service = ActiveStorage::Service.configure(:azure, SERVICE_CONFIGURATIONS)
+ end
+
+ teardown do
+ ActiveStorage::Blob.service = @old_service
+ end
+
+ test "creating new direct upload" do
+ checksum = Digest::MD5.base64digest("Hello")
+
+ post rails_direct_uploads_url, params: { blob: {
+ filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain" } }
+
+ @response.parsed_body.tap do |details|
+ assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed(details["signed_id"])
+ assert_equal "hello.txt", details["filename"]
+ assert_equal 6, details["byte_size"]
+ assert_equal checksum, details["checksum"]
+ assert_equal "text/plain", details["content_type"]
+ assert_match %r{#{@config[:storage_account_name]}\.blob\.core\.windows\.net/#{@config[:container]}}, details["direct_upload"]["url"]
+ assert_equal({ "Content-Type" => "text/plain", "Content-MD5" => checksum, "x-ms-blob-type" => "BlockBlob" }, details["direct_upload"]["headers"])
+ end
+ end
+ end
+else
+ puts "Skipping Azure Storage Direct Upload tests because no Azure Storage configuration was supplied"
+end
+
+class ActiveStorage::DiskDirectUploadsControllerTest < ActionDispatch::IntegrationTest
+ test "creating new direct upload" do
+ checksum = Digest::MD5.base64digest("Hello")
+
+ post rails_direct_uploads_url, params: { blob: {
+ filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain" } }
+
+ @response.parsed_body.tap do |details|
+ assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed(details["signed_id"])
+ assert_equal "hello.txt", details["filename"]
+ assert_equal 6, details["byte_size"]
+ assert_equal checksum, details["checksum"]
+ assert_equal "text/plain", details["content_type"]
+ assert_match(/rails\/active_storage\/disk/, details["direct_upload"]["url"])
+ assert_equal({ "Content-Type" => "text/plain" }, details["direct_upload"]["headers"])
+ end
+ end
+end
diff --git a/activestorage/test/controllers/disk_controller_test.rb b/activestorage/test/controllers/disk_controller_test.rb
new file mode 100644
index 0000000000..940dbf5918
--- /dev/null
+++ b/activestorage/test/controllers/disk_controller_test.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::DiskControllerTest < ActionDispatch::IntegrationTest
+ test "showing blob inline" do
+ blob = create_blob
+
+ get blob.service_url
+ assert_equal "inline; filename=\"hello.txt\"; filename*=UTF-8''hello.txt", @response.headers["Content-Disposition"]
+ assert_equal "text/plain", @response.headers["Content-Type"]
+ end
+
+ test "showing blob as attachment" do
+ blob = create_blob
+
+ get blob.service_url(disposition: :attachment)
+ assert_equal "attachment; filename=\"hello.txt\"; filename*=UTF-8''hello.txt", @response.headers["Content-Disposition"]
+ assert_equal "text/plain", @response.headers["Content-Type"]
+ end
+
+
+ test "directly uploading blob with integrity" do
+ data = "Something else entirely!"
+ blob = create_blob_before_direct_upload byte_size: data.size, checksum: Digest::MD5.base64digest(data)
+
+ put blob.service_url_for_direct_upload, params: data, headers: { "Content-Type" => "text/plain" }
+ assert_response :no_content
+ assert_equal data, blob.download
+ end
+
+ test "directly uploading blob without integrity" do
+ data = "Something else entirely!"
+ blob = create_blob_before_direct_upload byte_size: data.size, checksum: Digest::MD5.base64digest("bad data")
+
+ put blob.service_url_for_direct_upload, params: data
+ assert_response :unprocessable_entity
+ assert_not blob.service.exist?(blob.key)
+ end
+
+ test "directly uploading blob with mismatched content type" do
+ data = "Something else entirely!"
+ blob = create_blob_before_direct_upload byte_size: data.size, checksum: Digest::MD5.base64digest(data)
+
+ put blob.service_url_for_direct_upload, params: data, headers: { "Content-Type" => "application/octet-stream" }
+ assert_response :unprocessable_entity
+ assert_not blob.service.exist?(blob.key)
+ end
+
+ test "directly uploading blob with mismatched content length" do
+ data = "Something else entirely!"
+ blob = create_blob_before_direct_upload byte_size: data.size - 1, checksum: Digest::MD5.base64digest(data)
+
+ put blob.service_url_for_direct_upload, params: data, headers: { "Content-Type" => "text/plain" }
+ assert_response :unprocessable_entity
+ assert_not blob.service.exist?(blob.key)
+ end
+end
diff --git a/activestorage/test/controllers/previews_controller_test.rb b/activestorage/test/controllers/previews_controller_test.rb
new file mode 100644
index 0000000000..704a466160
--- /dev/null
+++ b/activestorage/test/controllers/previews_controller_test.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::PreviewsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @blob = create_file_blob filename: "report.pdf", content_type: "application/pdf"
+ end
+
+ test "showing preview inline" do
+ get rails_blob_preview_url(
+ filename: @blob.filename,
+ signed_blob_id: @blob.signed_id,
+ variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
+
+ assert @blob.preview_image.attached?
+ assert_redirected_to(/report\.png\?.*disposition=inline/)
+
+ image = read_image(@blob.preview_image.variant(resize: "100x100"))
+ assert_equal 77, image.width
+ assert_equal 100, image.height
+ end
+
+ test "showing preview with invalid signed blob ID" do
+ get rails_blob_preview_url(
+ filename: @blob.filename,
+ signed_blob_id: "invalid",
+ variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
+
+ assert_response :not_found
+ end
+end
diff --git a/activestorage/test/controllers/variants_controller_test.rb b/activestorage/test/controllers/variants_controller_test.rb
new file mode 100644
index 0000000000..a0642f9bed
--- /dev/null
+++ b/activestorage/test/controllers/variants_controller_test.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::VariantsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @blob = create_file_blob filename: "racecar.jpg"
+ end
+
+ test "showing variant inline" do
+ get rails_blob_variation_url(
+ filename: @blob.filename,
+ signed_blob_id: @blob.signed_id,
+ variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
+
+ assert_redirected_to(/racecar\.jpg\?.*disposition=inline/)
+
+ image = read_image(@blob.variant(resize: "100x100"))
+ assert_equal 100, image.width
+ assert_equal 67, image.height
+ end
+
+ test "showing variant with invalid signed blob ID" do
+ get rails_blob_variation_url(
+ filename: @blob.filename,
+ signed_blob_id: "invalid",
+ variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
+
+ assert_response :not_found
+ end
+end
diff --git a/activestorage/test/database/create_users_migration.rb b/activestorage/test/database/create_users_migration.rb
new file mode 100644
index 0000000000..fdba87cacf
--- /dev/null
+++ b/activestorage/test/database/create_users_migration.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class ActiveStorageCreateUsers < ActiveRecord::Migration[5.2]
+ def change
+ create_table :users do |t|
+ t.string :name
+ end
+ end
+end
diff --git a/activestorage/test/database/setup.rb b/activestorage/test/database/setup.rb
new file mode 100644
index 0000000000..705650a25d
--- /dev/null
+++ b/activestorage/test/database/setup.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require_relative "create_users_migration"
+
+ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
+ActiveRecord::Migrator.migrate File.expand_path("../../db/migrate", __dir__)
+ActiveStorageCreateUsers.migrate(:up)
diff --git a/activestorage/test/dummy/Rakefile b/activestorage/test/dummy/Rakefile
new file mode 100644
index 0000000000..c4f9523878
--- /dev/null
+++ b/activestorage/test/dummy/Rakefile
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+require_relative "config/application"
+
+Rails.application.load_tasks
diff --git a/activestorage/test/dummy/app/assets/config/manifest.js b/activestorage/test/dummy/app/assets/config/manifest.js
new file mode 100644
index 0000000000..a8adebe722
--- /dev/null
+++ b/activestorage/test/dummy/app/assets/config/manifest.js
@@ -0,0 +1,5 @@
+
+//= link_tree ../images
+//= link_directory ../javascripts .js
+//= link_directory ../stylesheets .css
+//= link active_storage_manifest.js
diff --git a/activestorage/test/dummy/app/assets/images/.keep b/activestorage/test/dummy/app/assets/images/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/activestorage/test/dummy/app/assets/images/.keep
diff --git a/activestorage/test/dummy/app/assets/javascripts/application.js b/activestorage/test/dummy/app/assets/javascripts/application.js
new file mode 100644
index 0000000000..e54c6461cc
--- /dev/null
+++ b/activestorage/test/dummy/app/assets/javascripts/application.js
@@ -0,0 +1,13 @@
+// This is a manifest file that'll be compiled into application.js, which will include all the files
+// listed below.
+//
+// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
+// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
+//
+// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+// compiled file. JavaScript code in this file should be added after the last require_* statement.
+//
+// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
+// about supported directives.
+//
+//= require_tree .
diff --git a/activestorage/test/dummy/app/assets/stylesheets/application.css b/activestorage/test/dummy/app/assets/stylesheets/application.css
new file mode 100644
index 0000000000..0ebd7fe829
--- /dev/null
+++ b/activestorage/test/dummy/app/assets/stylesheets/application.css
@@ -0,0 +1,15 @@
+/*
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
+ * listed below.
+ *
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
+ *
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
+ * files in this directory. Styles in this file should be added after the last require_* statement.
+ * It is generally better to create a new file per style scope.
+ *
+ *= require_tree .
+ *= require_self
+ */
diff --git a/activestorage/test/dummy/app/controllers/application_controller.rb b/activestorage/test/dummy/app/controllers/application_controller.rb
new file mode 100644
index 0000000000..280cc28ce2
--- /dev/null
+++ b/activestorage/test/dummy/app/controllers/application_controller.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ApplicationController < ActionController::Base
+ protect_from_forgery with: :exception
+end
diff --git a/activestorage/test/dummy/app/controllers/concerns/.keep b/activestorage/test/dummy/app/controllers/concerns/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/activestorage/test/dummy/app/controllers/concerns/.keep
diff --git a/activestorage/test/dummy/app/helpers/application_helper.rb b/activestorage/test/dummy/app/helpers/application_helper.rb
new file mode 100644
index 0000000000..15b06f0f67
--- /dev/null
+++ b/activestorage/test/dummy/app/helpers/application_helper.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+module ApplicationHelper
+end
diff --git a/activestorage/test/dummy/app/jobs/application_job.rb b/activestorage/test/dummy/app/jobs/application_job.rb
new file mode 100644
index 0000000000..d92ffddcb5
--- /dev/null
+++ b/activestorage/test/dummy/app/jobs/application_job.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+class ApplicationJob < ActiveJob::Base
+end
diff --git a/activestorage/test/dummy/app/models/application_record.rb b/activestorage/test/dummy/app/models/application_record.rb
new file mode 100644
index 0000000000..71fbba5b32
--- /dev/null
+++ b/activestorage/test/dummy/app/models/application_record.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ApplicationRecord < ActiveRecord::Base
+ self.abstract_class = true
+end
diff --git a/activestorage/test/dummy/app/models/concerns/.keep b/activestorage/test/dummy/app/models/concerns/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/activestorage/test/dummy/app/models/concerns/.keep
diff --git a/activestorage/test/dummy/app/views/layouts/application.html.erb b/activestorage/test/dummy/app/views/layouts/application.html.erb
new file mode 100644
index 0000000000..a6eb0174b7
--- /dev/null
+++ b/activestorage/test/dummy/app/views/layouts/application.html.erb
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Dummy</title>
+ <%= csrf_meta_tags %>
+
+ <%= stylesheet_link_tag 'application', media: 'all' %>
+ <%= javascript_include_tag 'application' %>
+ </head>
+
+ <body>
+ <%= yield %>
+ </body>
+</html>
diff --git a/activestorage/test/dummy/bin/bundle b/activestorage/test/dummy/bin/bundle
new file mode 100755
index 0000000000..5015ba6f8b
--- /dev/null
+++ b/activestorage/test/dummy/bin/bundle
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
+load Gem.bin_path("bundler", "bundle")
diff --git a/activestorage/test/dummy/bin/rails b/activestorage/test/dummy/bin/rails
new file mode 100755
index 0000000000..22f2d8deee
--- /dev/null
+++ b/activestorage/test/dummy/bin/rails
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+APP_PATH = File.expand_path("../config/application", __dir__)
+require_relative "../config/boot"
+require "rails/commands"
diff --git a/activestorage/test/dummy/bin/rake b/activestorage/test/dummy/bin/rake
new file mode 100755
index 0000000000..e436ea54a1
--- /dev/null
+++ b/activestorage/test/dummy/bin/rake
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require_relative "../config/boot"
+require "rake"
+Rake.application.run
diff --git a/activestorage/test/dummy/bin/yarn b/activestorage/test/dummy/bin/yarn
new file mode 100755
index 0000000000..c9b7498378
--- /dev/null
+++ b/activestorage/test/dummy/bin/yarn
@@ -0,0 +1,13 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+VENDOR_PATH = File.expand_path("..", __dir__)
+Dir.chdir(VENDOR_PATH) do
+ begin
+ exec "yarnpkg #{ARGV.join(" ")}"
+ rescue Errno::ENOENT
+ $stderr.puts "Yarn executable was not detected in the system."
+ $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
+ exit 1
+ end
+end
diff --git a/activestorage/test/dummy/config.ru b/activestorage/test/dummy/config.ru
new file mode 100644
index 0000000000..bff88d608a
--- /dev/null
+++ b/activestorage/test/dummy/config.ru
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+# This file is used by Rack-based servers to start the application.
+
+require_relative "config/environment"
+
+run Rails.application
diff --git a/activestorage/test/dummy/config/application.rb b/activestorage/test/dummy/config/application.rb
new file mode 100644
index 0000000000..bd14ac0b1a
--- /dev/null
+++ b/activestorage/test/dummy/config/application.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require_relative "boot"
+
+require "rails"
+require "active_model/railtie"
+require "active_job/railtie"
+require "active_record/railtie"
+require "action_controller/railtie"
+require "action_view/railtie"
+require "sprockets/railtie"
+require "active_storage/engine"
+
+Bundler.require(*Rails.groups)
+
+module Dummy
+ class Application < Rails::Application
+ config.load_defaults 5.2
+
+ config.active_storage.service = :local
+ end
+end
diff --git a/activestorage/test/dummy/config/boot.rb b/activestorage/test/dummy/config/boot.rb
new file mode 100644
index 0000000000..72516d9255
--- /dev/null
+++ b/activestorage/test/dummy/config/boot.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+# Set up gems listed in the Gemfile.
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__)
+
+require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
+$LOAD_PATH.unshift File.expand_path("../../../lib", __dir__)
+
+if %w[s server c console].any? { |a| ARGV.include?(a) }
+ puts "=> Booting Rails"
+end
diff --git a/activestorage/test/dummy/config/database.yml b/activestorage/test/dummy/config/database.yml
new file mode 100644
index 0000000000..0d02f24980
--- /dev/null
+++ b/activestorage/test/dummy/config/database.yml
@@ -0,0 +1,25 @@
+# SQLite version 3.x
+# gem install sqlite3
+#
+# Ensure the SQLite 3 gem is defined in your Gemfile
+# gem 'sqlite3'
+#
+default: &default
+ adapter: sqlite3
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ timeout: 5000
+
+development:
+ <<: *default
+ database: db/development.sqlite3
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ database: db/test.sqlite3
+
+production:
+ <<: *default
+ database: db/production.sqlite3
diff --git a/activestorage/test/dummy/config/environment.rb b/activestorage/test/dummy/config/environment.rb
new file mode 100644
index 0000000000..7df99e89c6
--- /dev/null
+++ b/activestorage/test/dummy/config/environment.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+# Load the Rails application.
+require_relative "application"
+
+# Initialize the Rails application.
+Rails.application.initialize!
diff --git a/activestorage/test/dummy/config/environments/development.rb b/activestorage/test/dummy/config/environments/development.rb
new file mode 100644
index 0000000000..47fc5bf25c
--- /dev/null
+++ b/activestorage/test/dummy/config/environments/development.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # In the development environment your application's code is reloaded on
+ # every request. This slows down response time but is perfect for development
+ # since you don't have to restart the web server when you make code changes.
+ config.cache_classes = false
+
+ # Do not eager load code on boot.
+ config.eager_load = false
+
+ # Show full error reports.
+ config.consider_all_requests_local = true
+
+ # Enable/disable caching. By default caching is disabled.
+ if Rails.root.join("tmp/caching-dev.txt").exist?
+ config.action_controller.perform_caching = true
+
+ config.cache_store = :memory_store
+ config.public_file_server.headers = {
+ "Cache-Control" => "public, max-age=#{2.days.seconds.to_i}"
+ }
+ else
+ config.action_controller.perform_caching = false
+
+ config.cache_store = :null_store
+ end
+
+ # Print deprecation notices to the Rails logger.
+ config.active_support.deprecation = :log
+
+ # Raise an error on page load if there are pending migrations.
+ config.active_record.migration_error = :page_load
+
+ # Debug mode disables concatenation and preprocessing of assets.
+ # This option may cause significant delays in view rendering with a large
+ # number of complex assets.
+ config.assets.debug = true
+
+ # Suppress logger output for asset requests.
+ config.assets.quiet = true
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
+
+ # Use an evented file watcher to asynchronously detect changes in source code,
+ # routes, locales, etc. This feature depends on the listen gem.
+ # config.file_watcher = ActiveSupport::EventedFileUpdateChecker
+end
diff --git a/activestorage/test/dummy/config/environments/production.rb b/activestorage/test/dummy/config/environments/production.rb
new file mode 100644
index 0000000000..34ac3bc561
--- /dev/null
+++ b/activestorage/test/dummy/config/environments/production.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # Code is not reloaded between requests.
+ config.cache_classes = true
+
+ # Eager load code on boot. This eager loads most of Rails and
+ # your application in memory, allowing both threaded web servers
+ # and those relying on copy on write to perform better.
+ # Rake tasks automatically ignore this option for performance.
+ config.eager_load = true
+
+ # Full error reports are disabled and caching is turned on.
+ config.consider_all_requests_local = false
+ config.action_controller.perform_caching = true
+
+ # Attempt to read encrypted secrets from `config/secrets.yml.enc`.
+ # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or
+ # `config/secrets.yml.key`.
+ config.read_encrypted_secrets = true
+
+ # Disable serving static files from the `/public` folder by default since
+ # Apache or NGINX already handles this.
+ config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
+
+ # Compress JavaScripts and CSS.
+ config.assets.js_compressor = :uglifier
+ # config.assets.css_compressor = :sass
+
+ # Do not fallback to assets pipeline if a precompiled asset is missed.
+ config.assets.compile = false
+
+ # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
+
+ # Enable serving of images, stylesheets, and JavaScripts from an asset server.
+ # config.action_controller.asset_host = 'http://assets.example.com'
+
+ # Specifies the header that your server uses for sending files.
+ # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
+
+ # Mount Action Cable outside main process or domain
+ # config.action_cable.mount_path = nil
+ # config.action_cable.url = 'wss://example.com/cable'
+ # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
+
+ # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
+ # config.force_ssl = true
+
+ # Use the lowest log level to ensure availability of diagnostic information
+ # when problems arise.
+ config.log_level = :debug
+
+ # Prepend all log lines with the following tags.
+ config.log_tags = [ :request_id ]
+
+ # Use a different cache store in production.
+ # config.cache_store = :mem_cache_store
+
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+ # the I18n.default_locale when a translation cannot be found).
+ config.i18n.fallbacks = true
+
+ # Send deprecation notices to registered listeners.
+ config.active_support.deprecation = :notify
+
+ # Use default logging formatter so that PID and timestamp are not suppressed.
+ config.log_formatter = ::Logger::Formatter.new
+
+ # Use a different logger for distributed setups.
+ # require 'syslog/logger'
+ # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
+
+ if ENV["RAILS_LOG_TO_STDOUT"].present?
+ logger = ActiveSupport::Logger.new(STDOUT)
+ logger.formatter = config.log_formatter
+ config.logger = ActiveSupport::TaggedLogging.new(logger)
+ end
+
+ # Do not dump schema after migrations.
+ config.active_record.dump_schema_after_migration = false
+end
diff --git a/activestorage/test/dummy/config/environments/test.rb b/activestorage/test/dummy/config/environments/test.rb
new file mode 100644
index 0000000000..74a802d98c
--- /dev/null
+++ b/activestorage/test/dummy/config/environments/test.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # The test environment is used exclusively to run your application's
+ # test suite. You never need to work with it otherwise. Remember that
+ # your test database is "scratch space" for the test suite and is wiped
+ # and recreated between test runs. Don't rely on the data there!
+ config.cache_classes = true
+
+ # Do not eager load code on boot. This avoids loading your whole application
+ # just for the purpose of running a single test. If you are using a tool that
+ # preloads Rails for running tests, you may have to set it to true.
+ config.eager_load = false
+
+ # Configure public file server for tests with Cache-Control for performance.
+ config.public_file_server.enabled = true
+ config.public_file_server.headers = {
+ "Cache-Control" => "public, max-age=#{1.hour.seconds.to_i}"
+ }
+
+ # Show full error reports and disable caching.
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+
+ # Raise exceptions instead of rendering exception templates.
+ config.action_dispatch.show_exceptions = false
+
+ # Print deprecation notices to the stderr.
+ config.active_support.deprecation = :stderr
+
+ # Disable request forgery protection in test environment.
+ config.action_controller.allow_forgery_protection = false
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
+end
diff --git a/activestorage/test/dummy/config/initializers/application_controller_renderer.rb b/activestorage/test/dummy/config/initializers/application_controller_renderer.rb
new file mode 100644
index 0000000000..315ac48a9a
--- /dev/null
+++ b/activestorage/test/dummy/config/initializers/application_controller_renderer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+# Be sure to restart your server when you modify this file.
+
+# ApplicationController.renderer.defaults.merge!(
+# http_host: 'example.org',
+# https: false
+# )
diff --git a/activestorage/test/dummy/config/initializers/assets.rb b/activestorage/test/dummy/config/initializers/assets.rb
new file mode 100644
index 0000000000..ba194685a2
--- /dev/null
+++ b/activestorage/test/dummy/config/initializers/assets.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# Be sure to restart your server when you modify this file.
+
+# Version of your assets, change this if you want to expire all your assets.
+Rails.application.config.assets.version = "1.0"
+
+# Add additional assets to the asset load path.
+# Rails.application.config.assets.paths << Emoji.images_path
+# Add Yarn node_modules folder to the asset load path.
+Rails.application.config.assets.paths << Rails.root.join("node_modules")
+
+# Precompile additional assets.
+# application.js, application.css, and all non-JS/CSS in the app/assets
+# folder are already added.
+# Rails.application.config.assets.precompile += %w( admin.js admin.css )
diff --git a/activestorage/test/dummy/config/initializers/backtrace_silencers.rb b/activestorage/test/dummy/config/initializers/backtrace_silencers.rb
new file mode 100644
index 0000000000..d0f0d3b5df
--- /dev/null
+++ b/activestorage/test/dummy/config/initializers/backtrace_silencers.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+# Be sure to restart your server when you modify this file.
+
+# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
+# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
+
+# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
+# Rails.backtrace_cleaner.remove_silencers!
diff --git a/activestorage/test/dummy/config/initializers/cookies_serializer.rb b/activestorage/test/dummy/config/initializers/cookies_serializer.rb
new file mode 100644
index 0000000000..ee8dff9c99
--- /dev/null
+++ b/activestorage/test/dummy/config/initializers/cookies_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+# Be sure to restart your server when you modify this file.
+
+# Specify a serializer for the signed and encrypted cookie jars.
+# Valid options are :json, :marshal, and :hybrid.
+Rails.application.config.action_dispatch.cookies_serializer = :json
diff --git a/activestorage/test/dummy/config/initializers/filter_parameter_logging.rb b/activestorage/test/dummy/config/initializers/filter_parameter_logging.rb
new file mode 100644
index 0000000000..7a4f47b4c2
--- /dev/null
+++ b/activestorage/test/dummy/config/initializers/filter_parameter_logging.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+# Be sure to restart your server when you modify this file.
+
+# Configure sensitive parameters which will be filtered from the log file.
+Rails.application.config.filter_parameters += [:password]
diff --git a/activestorage/test/dummy/config/initializers/inflections.rb b/activestorage/test/dummy/config/initializers/inflections.rb
new file mode 100644
index 0000000000..aa7435fbc9
--- /dev/null
+++ b/activestorage/test/dummy/config/initializers/inflections.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+# Be sure to restart your server when you modify this file.
+
+# Add new inflection rules using the following format. Inflections
+# are locale specific, and you may define rules for as many different
+# locales as you wish. All of these examples are active by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.plural /^(ox)$/i, '\1en'
+# inflect.singular /^(ox)en/i, '\1'
+# inflect.irregular 'person', 'people'
+# inflect.uncountable %w( fish sheep )
+# end
+
+# These inflection rules are supported but not enabled by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.acronym 'RESTful'
+# end
diff --git a/activestorage/test/dummy/config/initializers/mime_types.rb b/activestorage/test/dummy/config/initializers/mime_types.rb
new file mode 100644
index 0000000000..6e1d16f027
--- /dev/null
+++ b/activestorage/test/dummy/config/initializers/mime_types.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+# Be sure to restart your server when you modify this file.
+
+# Add new mime types for use in respond_to blocks:
+# Mime::Type.register "text/richtext", :rtf
diff --git a/activestorage/test/dummy/config/initializers/wrap_parameters.rb b/activestorage/test/dummy/config/initializers/wrap_parameters.rb
new file mode 100644
index 0000000000..2f3c0db471
--- /dev/null
+++ b/activestorage/test/dummy/config/initializers/wrap_parameters.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# Be sure to restart your server when you modify this file.
+
+# This file contains settings for ActionController::ParamsWrapper which
+# is enabled by default.
+
+# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
+ActiveSupport.on_load(:action_controller) do
+ wrap_parameters format: [:json]
+end
+
+# To enable root element in JSON for ActiveRecord objects.
+# ActiveSupport.on_load(:active_record) do
+# self.include_root_in_json = true
+# end
diff --git a/activestorage/test/dummy/config/routes.rb b/activestorage/test/dummy/config/routes.rb
new file mode 100644
index 0000000000..edf04d2d63
--- /dev/null
+++ b/activestorage/test/dummy/config/routes.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+Rails.application.routes.draw do
+end
diff --git a/activestorage/test/dummy/config/secrets.yml b/activestorage/test/dummy/config/secrets.yml
new file mode 100644
index 0000000000..77d1fc383a
--- /dev/null
+++ b/activestorage/test/dummy/config/secrets.yml
@@ -0,0 +1,32 @@
+# Be sure to restart your server when you modify this file.
+
+# Your secret key is used for verifying the integrity of signed cookies.
+# If you change this key, all old signed cookies will become invalid!
+
+# Make sure the secret is at least 30 characters and all random,
+# no regular words or you'll be exposed to dictionary attacks.
+# You can use `rails secret` to generate a secure secret key.
+
+# Make sure the secrets in this file are kept private
+# if you're sharing your code publicly.
+
+# Shared secrets are available across all environments.
+
+# shared:
+# api_key: a1B2c3D4e5F6
+
+# Environmental secrets are only available for that specific environment.
+
+development:
+ secret_key_base: e0ef5744b10d988669be6b2660c259749779964f3dcb487fd6199743b3558e2d89f7681d6a15d16d144e28979cbdae41885f4fb4c2cf56ff92ac22df282ffb66
+
+test:
+ secret_key_base: 6fb1f3a828a8dcd6ac8dc07b43be4a5265ad64379120d417252a1578fe1f790e7b85ade4f95994de1ac8fb78581690de6e3a6ac4af36a0f0139667418c750d05
+
+# Do not keep production secrets in the unencrypted secrets file.
+# Instead, either read values from the environment.
+# Or, use `bin/rails secrets:setup` to configure encrypted secrets
+# and move the `production:` environment over there.
+
+production:
+ secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
diff --git a/activestorage/test/dummy/config/spring.rb b/activestorage/test/dummy/config/spring.rb
new file mode 100644
index 0000000000..ff5ba06b6d
--- /dev/null
+++ b/activestorage/test/dummy/config/spring.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+%w(
+ .ruby-version
+ .rbenv-vars
+ tmp/restart.txt
+ tmp/caching-dev.txt
+).each { |path| Spring.watch(path) }
diff --git a/activestorage/test/dummy/config/storage.yml b/activestorage/test/dummy/config/storage.yml
new file mode 100644
index 0000000000..2c6762e0d6
--- /dev/null
+++ b/activestorage/test/dummy/config/storage.yml
@@ -0,0 +1,3 @@
+local:
+ service: Disk
+ root: <%= Rails.root.join("storage") %>
diff --git a/activestorage/test/dummy/lib/assets/.keep b/activestorage/test/dummy/lib/assets/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/activestorage/test/dummy/lib/assets/.keep
diff --git a/activestorage/test/dummy/log/.keep b/activestorage/test/dummy/log/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/activestorage/test/dummy/log/.keep
diff --git a/activestorage/test/dummy/package.json b/activestorage/test/dummy/package.json
new file mode 100644
index 0000000000..caa2d7bb3f
--- /dev/null
+++ b/activestorage/test/dummy/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "dummy",
+ "private": true,
+ "dependencies": {}
+}
diff --git a/activestorage/test/dummy/public/404.html b/activestorage/test/dummy/public/404.html
new file mode 100644
index 0000000000..2be3af26fc
--- /dev/null
+++ b/activestorage/test/dummy/public/404.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>The page you were looking for doesn't exist (404)</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1">
+ <style>
+ .rails-default-error-page {
+ background-color: #EFEFEF;
+ color: #2E2F30;
+ text-align: center;
+ font-family: arial, sans-serif;
+ margin: 0;
+ }
+
+ .rails-default-error-page div.dialog {
+ width: 95%;
+ max-width: 33em;
+ margin: 4em auto 0;
+ }
+
+ .rails-default-error-page div.dialog > div {
+ border: 1px solid #CCC;
+ border-right-color: #999;
+ border-left-color: #999;
+ border-bottom-color: #BBB;
+ border-top: #B00100 solid 4px;
+ border-top-left-radius: 9px;
+ border-top-right-radius: 9px;
+ background-color: white;
+ padding: 7px 12% 0;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+ }
+
+ .rails-default-error-page h1 {
+ font-size: 100%;
+ color: #730E15;
+ line-height: 1.5em;
+ }
+
+ .rails-default-error-page div.dialog > p {
+ margin: 0 0 1em;
+ padding: 1em;
+ background-color: #F7F7F7;
+ border: 1px solid #CCC;
+ border-right-color: #999;
+ border-left-color: #999;
+ border-bottom-color: #999;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ border-top-color: #DADADA;
+ color: #666;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+ }
+ </style>
+</head>
+
+<body class="rails-default-error-page">
+ <!-- This file lives in public/404.html -->
+ <div class="dialog">
+ <div>
+ <h1>The page you were looking for doesn't exist.</h1>
+ <p>You may have mistyped the address or the page may have moved.</p>
+ </div>
+ <p>If you are the application owner check the logs for more information.</p>
+ </div>
+</body>
+</html>
diff --git a/activestorage/test/dummy/public/422.html b/activestorage/test/dummy/public/422.html
new file mode 100644
index 0000000000..c08eac0d1d
--- /dev/null
+++ b/activestorage/test/dummy/public/422.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>The change you wanted was rejected (422)</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1">
+ <style>
+ .rails-default-error-page {
+ background-color: #EFEFEF;
+ color: #2E2F30;
+ text-align: center;
+ font-family: arial, sans-serif;
+ margin: 0;
+ }
+
+ .rails-default-error-page div.dialog {
+ width: 95%;
+ max-width: 33em;
+ margin: 4em auto 0;
+ }
+
+ .rails-default-error-page div.dialog > div {
+ border: 1px solid #CCC;
+ border-right-color: #999;
+ border-left-color: #999;
+ border-bottom-color: #BBB;
+ border-top: #B00100 solid 4px;
+ border-top-left-radius: 9px;
+ border-top-right-radius: 9px;
+ background-color: white;
+ padding: 7px 12% 0;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+ }
+
+ .rails-default-error-page h1 {
+ font-size: 100%;
+ color: #730E15;
+ line-height: 1.5em;
+ }
+
+ .rails-default-error-page div.dialog > p {
+ margin: 0 0 1em;
+ padding: 1em;
+ background-color: #F7F7F7;
+ border: 1px solid #CCC;
+ border-right-color: #999;
+ border-left-color: #999;
+ border-bottom-color: #999;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ border-top-color: #DADADA;
+ color: #666;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+ }
+ </style>
+</head>
+
+<body class="rails-default-error-page">
+ <!-- This file lives in public/422.html -->
+ <div class="dialog">
+ <div>
+ <h1>The change you wanted was rejected.</h1>
+ <p>Maybe you tried to change something you didn't have access to.</p>
+ </div>
+ <p>If you are the application owner check the logs for more information.</p>
+ </div>
+</body>
+</html>
diff --git a/activestorage/test/dummy/public/500.html b/activestorage/test/dummy/public/500.html
new file mode 100644
index 0000000000..78a030af22
--- /dev/null
+++ b/activestorage/test/dummy/public/500.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>We're sorry, but something went wrong (500)</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1">
+ <style>
+ .rails-default-error-page {
+ background-color: #EFEFEF;
+ color: #2E2F30;
+ text-align: center;
+ font-family: arial, sans-serif;
+ margin: 0;
+ }
+
+ .rails-default-error-page div.dialog {
+ width: 95%;
+ max-width: 33em;
+ margin: 4em auto 0;
+ }
+
+ .rails-default-error-page div.dialog > div {
+ border: 1px solid #CCC;
+ border-right-color: #999;
+ border-left-color: #999;
+ border-bottom-color: #BBB;
+ border-top: #B00100 solid 4px;
+ border-top-left-radius: 9px;
+ border-top-right-radius: 9px;
+ background-color: white;
+ padding: 7px 12% 0;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+ }
+
+ .rails-default-error-page h1 {
+ font-size: 100%;
+ color: #730E15;
+ line-height: 1.5em;
+ }
+
+ .rails-default-error-page div.dialog > p {
+ margin: 0 0 1em;
+ padding: 1em;
+ background-color: #F7F7F7;
+ border: 1px solid #CCC;
+ border-right-color: #999;
+ border-left-color: #999;
+ border-bottom-color: #999;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ border-top-color: #DADADA;
+ color: #666;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+ }
+ </style>
+</head>
+
+<body class="rails-default-error-page">
+ <!-- This file lives in public/500.html -->
+ <div class="dialog">
+ <div>
+ <h1>We're sorry, but something went wrong.</h1>
+ </div>
+ <p>If you are the application owner check the logs for more information.</p>
+ </div>
+</body>
+</html>
diff --git a/activestorage/test/dummy/public/apple-touch-icon-precomposed.png b/activestorage/test/dummy/public/apple-touch-icon-precomposed.png
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/activestorage/test/dummy/public/apple-touch-icon-precomposed.png
diff --git a/activestorage/test/dummy/public/apple-touch-icon.png b/activestorage/test/dummy/public/apple-touch-icon.png
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/activestorage/test/dummy/public/apple-touch-icon.png
diff --git a/activestorage/test/dummy/public/favicon.ico b/activestorage/test/dummy/public/favicon.ico
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/activestorage/test/dummy/public/favicon.ico
diff --git a/activestorage/test/fixtures/files/icon.psd b/activestorage/test/fixtures/files/icon.psd
new file mode 100644
index 0000000000..631fceeaab
--- /dev/null
+++ b/activestorage/test/fixtures/files/icon.psd
Binary files differ
diff --git a/activestorage/test/fixtures/files/icon.svg b/activestorage/test/fixtures/files/icon.svg
new file mode 100644
index 0000000000..6cfb0e241e
--- /dev/null
+++ b/activestorage/test/fixtures/files/icon.svg
@@ -0,0 +1,13 @@
+<!-- The XML declaration is intentionally omitted. -->
+<svg width="792px" height="584px" viewBox="0 0 792 584" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="M9.51802657,28.724593 C9.51802657,18.2155955 18.1343454,9.60822622 28.6542694,9.60822622 L763.245541,9.60822622 C773.765465,9.60822622 782.381784,18.2155955 782.381784,28.724593 L782.381784,584 L792,584 L792,28.724593 C792,12.911054 779.075522,0 763.245541,0 L28.7544592,0 C12.9244782,0 0,12.911054 0,28.724593 L0,584 L9.61821632,584 C9.51802657,584 9.51802657,28.724593 9.51802657,28.724593 L9.51802657,28.724593 Z" id="Shape" opacity="0.3" fill="#CCCCCC"></path>
+ <circle id="Oval" fill="#FFCC33" cx="119.1" cy="147.2" r="33"></circle>
+ <circle id="Oval" fill="#3399FF" cx="119.1" cy="281.1" r="33"></circle>
+ <circle id="Oval" fill="#FF3333" cx="676.1" cy="376.8" r="33"></circle>
+ <circle id="Oval" fill="#FFCC33" cx="119.1" cy="477.2" r="33"></circle>
+ <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="130.5" width="442.1" height="33.5"></rect>
+ <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="183.1" width="442.1" height="33.5"></rect>
+ <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="265.8" width="442.1" height="33.5"></rect>
+ <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="363.4" width="442.1" height="33.5"></rect>
+ <rect id="Rectangle-path" opacity="0.75" fill="#CCCCCC" x="176.5" y="465.3" width="442.1" height="33.5"></rect>
+</svg>
diff --git a/activestorage/test/fixtures/files/image.gif b/activestorage/test/fixtures/files/image.gif
new file mode 100644
index 0000000000..90c05f671c
--- /dev/null
+++ b/activestorage/test/fixtures/files/image.gif
Binary files differ
diff --git a/activestorage/test/fixtures/files/racecar.jpg b/activestorage/test/fixtures/files/racecar.jpg
new file mode 100644
index 0000000000..934b4caa22
--- /dev/null
+++ b/activestorage/test/fixtures/files/racecar.jpg
Binary files differ
diff --git a/activestorage/test/fixtures/files/report.pdf b/activestorage/test/fixtures/files/report.pdf
new file mode 100644
index 0000000000..cccb9b5d64
--- /dev/null
+++ b/activestorage/test/fixtures/files/report.pdf
Binary files differ
diff --git a/activestorage/test/fixtures/files/rotated_video.mp4 b/activestorage/test/fixtures/files/rotated_video.mp4
new file mode 100644
index 0000000000..4c7a4e9e57
--- /dev/null
+++ b/activestorage/test/fixtures/files/rotated_video.mp4
Binary files differ
diff --git a/activestorage/test/fixtures/files/video.mp4 b/activestorage/test/fixtures/files/video.mp4
new file mode 100644
index 0000000000..8fb1c5b24d
--- /dev/null
+++ b/activestorage/test/fixtures/files/video.mp4
Binary files differ
diff --git a/activestorage/test/fixtures/files/video_without_video_stream.mp4 b/activestorage/test/fixtures/files/video_without_video_stream.mp4
new file mode 100644
index 0000000000..e6a55f868b
--- /dev/null
+++ b/activestorage/test/fixtures/files/video_without_video_stream.mp4
Binary files differ
diff --git a/activestorage/test/models/attachments_test.rb b/activestorage/test/models/attachments_test.rb
new file mode 100644
index 0000000000..20eec3c220
--- /dev/null
+++ b/activestorage/test/models/attachments_test.rb
@@ -0,0 +1,347 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
+ include ActiveJob::TestHelper
+
+ setup { @user = User.create!(name: "DHH") }
+
+ teardown { ActiveStorage::Blob.all.each(&:purge) }
+
+ test "attach existing blob" do
+ @user.avatar.attach create_blob(filename: "funky.jpg")
+ assert_equal "funky.jpg", @user.avatar.filename.to_s
+ end
+
+ test "attach existing blob from a signed ID" do
+ @user.avatar.attach create_blob(filename: "funky.jpg").signed_id
+ assert_equal "funky.jpg", @user.avatar.filename.to_s
+ end
+
+ test "attach new blob from a Hash" do
+ @user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg"
+ assert_equal "town.jpg", @user.avatar.filename.to_s
+ end
+
+ test "attach new blob from an UploadedFile" do
+ file = file_fixture "racecar.jpg"
+ @user.avatar.attach Rack::Test::UploadedFile.new file.to_s
+ assert_equal "racecar.jpg", @user.avatar.filename.to_s
+ end
+
+ test "replace attached blob" do
+ @user.avatar.attach create_blob(filename: "funky.jpg")
+
+ perform_enqueued_jobs do
+ assert_no_difference -> { ActiveStorage::Blob.count } do
+ @user.avatar.attach create_blob(filename: "town.jpg")
+ end
+ end
+
+ assert_equal "town.jpg", @user.avatar.filename.to_s
+ end
+
+ test "replace attached blob unsuccessfully" do
+ @user.avatar.attach create_blob(filename: "funky.jpg")
+
+ perform_enqueued_jobs do
+ assert_raises do
+ @user.avatar.attach nil
+ end
+ end
+
+ assert_equal "funky.jpg", @user.reload.avatar.filename.to_s
+ assert ActiveStorage::Blob.service.exist?(@user.avatar.key)
+ end
+
+ test "attach blob to new record" do
+ user = User.new(name: "Jason")
+
+ assert_no_changes -> { user.new_record? } do
+ assert_no_difference -> { ActiveStorage::Attachment.count } do
+ user.avatar.attach create_blob(filename: "funky.jpg")
+ end
+ end
+
+ assert user.avatar.attached?
+ assert_equal "funky.jpg", user.avatar.filename.to_s
+
+ assert_difference -> { ActiveStorage::Attachment.count }, +1 do
+ user.save!
+ end
+
+ assert user.reload.avatar.attached?
+ assert_equal "funky.jpg", user.avatar.filename.to_s
+ end
+
+ test "build new record with attached blob" do
+ assert_no_difference -> { ActiveStorage::Attachment.count } do
+ @user = User.new(name: "Jason", avatar: { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" })
+ end
+
+ assert @user.new_record?
+ assert @user.avatar.attached?
+ assert_equal "town.jpg", @user.avatar.filename.to_s
+
+ @user.save!
+ assert @user.reload.avatar.attached?
+ assert_equal "town.jpg", @user.avatar.filename.to_s
+ end
+
+ test "access underlying associations of new blob" do
+ @user.avatar.attach create_blob(filename: "funky.jpg")
+ assert_equal @user, @user.avatar_attachment.record
+ assert_equal @user.avatar_attachment.blob, @user.avatar_blob
+ assert_equal "funky.jpg", @user.avatar_attachment.blob.filename.to_s
+ end
+
+ test "analyze newly-attached blob" do
+ perform_enqueued_jobs do
+ @user.avatar.attach create_file_blob
+ end
+
+ assert_equal 4104, @user.avatar.reload.metadata[:width]
+ assert_equal 2736, @user.avatar.metadata[:height]
+ end
+
+ test "analyze attached blob only once" do
+ blob = create_file_blob
+
+ perform_enqueued_jobs do
+ @user.avatar.attach blob
+ end
+
+ assert blob.reload.analyzed?
+
+ @user.avatar.attachment.destroy
+
+ assert_no_enqueued_jobs do
+ @user.reload.avatar.attach blob
+ end
+ end
+
+ test "preserve existing metadata when analyzing a newly-attached blob" do
+ blob = create_file_blob(metadata: { foo: "bar" })
+
+ perform_enqueued_jobs do
+ @user.avatar.attach blob
+ end
+
+ assert_equal "bar", blob.reload.metadata[:foo]
+ end
+
+ test "detach blob" do
+ @user.avatar.attach create_blob(filename: "funky.jpg")
+ avatar_blob_id = @user.avatar.blob.id
+ avatar_key = @user.avatar.key
+
+ @user.avatar.detach
+ assert_not @user.avatar.attached?
+ assert ActiveStorage::Blob.exists?(avatar_blob_id)
+ assert ActiveStorage::Blob.service.exist?(avatar_key)
+ end
+
+ test "purge attached blob" do
+ @user.avatar.attach create_blob(filename: "funky.jpg")
+ avatar_key = @user.avatar.key
+
+ @user.avatar.purge
+ assert_not @user.avatar.attached?
+ assert_not ActiveStorage::Blob.service.exist?(avatar_key)
+ end
+
+ test "purge attached blob later when the record is destroyed" do
+ @user.avatar.attach create_blob(filename: "funky.jpg")
+ avatar_key = @user.avatar.key
+
+ perform_enqueued_jobs do
+ @user.destroy
+
+ assert_nil ActiveStorage::Blob.find_by(key: avatar_key)
+ assert_not ActiveStorage::Blob.service.exist?(avatar_key)
+ end
+ end
+
+ test "find with attached blob" do
+ records = %w[alice bob].map do |name|
+ User.create!(name: name).tap do |user|
+ user.avatar.attach create_blob(filename: "#{name}.jpg")
+ end
+ end
+
+ users = User.where(id: records.map(&:id)).with_attached_avatar.all
+
+ assert_equal "alice.jpg", users.first.avatar.filename.to_s
+ assert_equal "bob.jpg", users.second.avatar.filename.to_s
+ end
+
+
+ test "attach existing blobs" do
+ @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "wonky.jpg")
+
+ assert_equal "funky.jpg", @user.highlights.first.filename.to_s
+ assert_equal "wonky.jpg", @user.highlights.second.filename.to_s
+ end
+
+ test "attach new blobs" do
+ @user.highlights.attach(
+ { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" },
+ { io: StringIO.new("IT"), filename: "country.jpg", content_type: "image/jpg" })
+
+ assert_equal "town.jpg", @user.highlights.first.filename.to_s
+ assert_equal "country.jpg", @user.highlights.second.filename.to_s
+ end
+
+ test "attach blobs to new record" do
+ user = User.new(name: "Jason")
+
+ assert_no_changes -> { user.new_record? } do
+ assert_no_difference -> { ActiveStorage::Attachment.count } do
+ user.highlights.attach(
+ { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" },
+ { io: StringIO.new("IT"), filename: "country.jpg", content_type: "image/jpg" })
+ end
+ end
+
+ assert user.highlights.attached?
+ assert_equal "town.jpg", user.highlights.first.filename.to_s
+ assert_equal "country.jpg", user.highlights.second.filename.to_s
+
+ assert_difference -> { ActiveStorage::Attachment.count }, +2 do
+ user.save!
+ end
+
+ assert user.reload.highlights.attached?
+ assert_equal "town.jpg", user.highlights.first.filename.to_s
+ assert_equal "country.jpg", user.highlights.second.filename.to_s
+ end
+
+ test "build new record with attached blobs" do
+ assert_no_difference -> { ActiveStorage::Attachment.count } do
+ @user = User.new(name: "Jason", highlights: [
+ { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" },
+ { io: StringIO.new("IT"), filename: "country.jpg", content_type: "image/jpg" }])
+ end
+
+ assert @user.new_record?
+ assert @user.highlights.attached?
+ assert_equal "town.jpg", @user.highlights.first.filename.to_s
+ assert_equal "country.jpg", @user.highlights.second.filename.to_s
+
+ @user.save!
+ assert @user.reload.highlights.attached?
+ assert_equal "town.jpg", @user.highlights.first.filename.to_s
+ assert_equal "country.jpg", @user.highlights.second.filename.to_s
+ end
+
+ test "find attached blobs" do
+ @user.highlights.attach(
+ { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" },
+ { io: StringIO.new("IT"), filename: "country.jpg", content_type: "image/jpg" })
+
+ highlights = User.where(id: @user.id).with_attached_highlights.first.highlights
+
+ assert_equal "town.jpg", highlights.first.filename.to_s
+ assert_equal "country.jpg", highlights.second.filename.to_s
+ end
+
+ test "access underlying associations of new blobs" do
+ @user.highlights.attach(
+ { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" },
+ { io: StringIO.new("IT"), filename: "country.jpg", content_type: "image/jpg" })
+
+ assert_equal @user, @user.highlights_attachments.first.record
+ assert_equal @user.highlights_attachments.collect(&:blob).sort, @user.highlights_blobs.sort
+ assert_equal "town.jpg", @user.highlights_attachments.first.blob.filename.to_s
+ end
+
+ test "analyze newly-attached blobs" do
+ perform_enqueued_jobs do
+ @user.highlights.attach(
+ create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg"),
+ create_file_blob(filename: "video.mp4", content_type: "video/mp4"))
+ end
+
+ assert_equal 4104, @user.highlights.first.metadata[:width]
+ assert_equal 2736, @user.highlights.first.metadata[:height]
+
+ assert_equal 640, @user.highlights.second.metadata[:width]
+ assert_equal 480, @user.highlights.second.metadata[:height]
+ end
+
+ test "analyze attached blobs only once" do
+ blobs = [
+ create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg"),
+ create_file_blob(filename: "video.mp4", content_type: "video/mp4")
+ ]
+
+ perform_enqueued_jobs do
+ @user.highlights.attach(blobs)
+ end
+
+ assert blobs.each(&:reload).all?(&:analyzed?)
+
+ @user.highlights.attachments.destroy_all
+
+ assert_no_enqueued_jobs do
+ @user.highlights.attach(blobs)
+ end
+ end
+
+ test "preserve existing metadata when analyzing newly-attached blobs" do
+ blobs = [
+ create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg", metadata: { foo: "bar" }),
+ create_file_blob(filename: "video.mp4", content_type: "video/mp4", metadata: { foo: "bar" })
+ ]
+
+ perform_enqueued_jobs do
+ @user.highlights.attach(blobs)
+ end
+
+ blobs.each do |blob|
+ assert_equal "bar", blob.reload.metadata[:foo]
+ end
+ end
+
+ test "detach blobs" do
+ @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "wonky.jpg")
+ highlight_blob_ids = @user.highlights.collect { |highlight| highlight.blob.id }
+ highlight_keys = @user.highlights.collect(&:key)
+
+ @user.highlights.detach
+ assert_not @user.highlights.attached?
+
+ assert ActiveStorage::Blob.exists?(highlight_blob_ids.first)
+ assert ActiveStorage::Blob.exists?(highlight_blob_ids.second)
+
+ assert ActiveStorage::Blob.service.exist?(highlight_keys.first)
+ assert ActiveStorage::Blob.service.exist?(highlight_keys.second)
+ end
+
+ test "purge attached blobs" do
+ @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "wonky.jpg")
+ highlight_keys = @user.highlights.collect(&:key)
+
+ @user.highlights.purge
+ assert_not @user.highlights.attached?
+ assert_not ActiveStorage::Blob.service.exist?(highlight_keys.first)
+ assert_not ActiveStorage::Blob.service.exist?(highlight_keys.second)
+ end
+
+ test "purge attached blobs later when the record is destroyed" do
+ @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "wonky.jpg")
+ highlight_keys = @user.highlights.collect(&:key)
+
+ perform_enqueued_jobs do
+ @user.destroy
+
+ assert_nil ActiveStorage::Blob.find_by(key: highlight_keys.first)
+ assert_not ActiveStorage::Blob.service.exist?(highlight_keys.first)
+
+ assert_nil ActiveStorage::Blob.find_by(key: highlight_keys.second)
+ assert_not ActiveStorage::Blob.service.exist?(highlight_keys.second)
+ end
+ end
+end
diff --git a/activestorage/test/models/blob_test.rb b/activestorage/test/models/blob_test.rb
new file mode 100644
index 0000000000..f94e65ed77
--- /dev/null
+++ b/activestorage/test/models/blob_test.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::BlobTest < ActiveSupport::TestCase
+ test "create after upload sets byte size and checksum" do
+ data = "Hello world!"
+ blob = create_blob data: data
+
+ assert_equal data, blob.download
+ assert_equal data.length, blob.byte_size
+ assert_equal Digest::MD5.base64digest(data), blob.checksum
+ end
+
+ test "text?" do
+ blob = create_blob data: "Hello world!"
+ assert blob.text?
+ assert_not blob.audio?
+ end
+
+ test "download yields chunks" do
+ blob = create_blob data: "a" * 75.kilobytes
+ chunks = []
+
+ blob.download do |chunk|
+ chunks << chunk
+ end
+
+ assert_equal 2, chunks.size
+ assert_equal "a" * 64.kilobytes, chunks.first
+ assert_equal "a" * 11.kilobytes, chunks.second
+ end
+
+ test "urls expiring in 5 minutes" do
+ blob = create_blob
+
+ freeze_time do
+ assert_equal expected_url_for(blob), blob.service_url
+ assert_equal expected_url_for(blob, disposition: :attachment), blob.service_url(disposition: :attachment)
+ end
+ end
+
+ test "purge deletes file from external service" do
+ blob = create_blob
+
+ blob.purge
+ assert_not ActiveStorage::Blob.service.exist?(blob.key)
+ end
+
+ test "purge deletes variants from external service" do
+ blob = create_file_blob
+ variant = blob.variant(resize: "100>").processed
+
+ blob.purge
+ assert_not ActiveStorage::Blob.service.exist?(variant.key)
+ end
+
+ private
+ def expected_url_for(blob, disposition: :inline)
+ query_string = { content_type: blob.content_type, disposition: "#{disposition}; #{blob.filename.parameters}" }.to_param
+ "/rails/active_storage/disk/#{ActiveStorage.verifier.generate(blob.key, expires_in: 5.minutes, purpose: :blob_key)}/#{blob.filename}?#{query_string}"
+ end
+end
diff --git a/activestorage/test/models/filename/parameters_test.rb b/activestorage/test/models/filename/parameters_test.rb
new file mode 100644
index 0000000000..431be00639
--- /dev/null
+++ b/activestorage/test/models/filename/parameters_test.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActiveStorage::Filename::ParametersTest < ActiveSupport::TestCase
+ test "parameterizing a Latin filename" do
+ filename = ActiveStorage::Filename.new("racecar.jpg")
+
+ assert_equal %(filename="racecar.jpg"), filename.parameters.ascii
+ assert_equal "filename*=UTF-8''racecar.jpg", filename.parameters.utf8
+ assert_equal "#{filename.parameters.ascii}; #{filename.parameters.utf8}", filename.parameters.combined
+ assert_equal filename.parameters.combined, filename.parameters.to_s
+ end
+
+ test "parameterizing a Latin filename with accented characters" do
+ filename = ActiveStorage::Filename.new("råcëçâr.jpg")
+
+ assert_equal %(filename="racecar.jpg"), filename.parameters.ascii
+ assert_equal "filename*=UTF-8''r%C3%A5c%C3%AB%C3%A7%C3%A2r.jpg", filename.parameters.utf8
+ assert_equal "#{filename.parameters.ascii}; #{filename.parameters.utf8}", filename.parameters.combined
+ assert_equal filename.parameters.combined, filename.parameters.to_s
+ end
+
+ test "parameterizing a non-Latin filename" do
+ filename = ActiveStorage::Filename.new("автомобиль.jpg")
+
+ assert_equal %(filename="%3F%3F%3F%3F%3F%3F%3F%3F%3F%3F.jpg"), filename.parameters.ascii
+ assert_equal "filename*=UTF-8''%D0%B0%D0%B2%D1%82%D0%BE%D0%BC%D0%BE%D0%B1%D0%B8%D0%BB%D1%8C.jpg", filename.parameters.utf8
+ assert_equal "#{filename.parameters.ascii}; #{filename.parameters.utf8}", filename.parameters.combined
+ assert_equal filename.parameters.combined, filename.parameters.to_s
+ end
+end
diff --git a/activestorage/test/models/filename_test.rb b/activestorage/test/models/filename_test.rb
new file mode 100644
index 0000000000..88405e41c0
--- /dev/null
+++ b/activestorage/test/models/filename_test.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActiveStorage::FilenameTest < ActiveSupport::TestCase
+ test "base" do
+ assert_equal "racecar", ActiveStorage::Filename.new("racecar.jpg").base
+ assert_equal "race.car", ActiveStorage::Filename.new("race.car.jpg").base
+ assert_equal "racecar", ActiveStorage::Filename.new("racecar").base
+ end
+
+ test "extension with delimiter" do
+ assert_equal ".jpg", ActiveStorage::Filename.new("racecar.jpg").extension_with_delimiter
+ assert_equal ".jpg", ActiveStorage::Filename.new("race.car.jpg").extension_with_delimiter
+ assert_equal "", ActiveStorage::Filename.new("racecar").extension_with_delimiter
+ end
+
+ test "extension without delimiter" do
+ assert_equal "jpg", ActiveStorage::Filename.new("racecar.jpg").extension_without_delimiter
+ assert_equal "jpg", ActiveStorage::Filename.new("race.car.jpg").extension_without_delimiter
+ assert_equal "", ActiveStorage::Filename.new("racecar").extension_without_delimiter
+ end
+
+ test "sanitize" do
+ "%$|:;/\t\r\n\\".each_char do |character|
+ filename = ActiveStorage::Filename.new("foo#{character}bar.pdf")
+ assert_equal "foo-bar.pdf", filename.sanitized
+ assert_equal "foo-bar.pdf", filename.to_s
+ end
+ end
+
+ test "sanitize transcodes to valid UTF-8" do
+ { "\xF6".dup.force_encoding(Encoding::ISO8859_1) => "ö",
+ "\xC3".dup.force_encoding(Encoding::ISO8859_1) => "Ã",
+ "\xAD" => "�",
+ "\xCF" => "�",
+ "\x00" => "",
+ }.each do |actual, expected|
+ assert_equal expected, ActiveStorage::Filename.new(actual).sanitized
+ end
+ end
+
+ test "strips RTL override chars used to spoof unsafe executables as docs" do
+ # Would be displayed in Windows as "evilexe.pdf" due to the right-to-left
+ # (RTL) override char!
+ assert_equal "evil-fdp.exe", ActiveStorage::Filename.new("evil\u{202E}fdp.exe").sanitized
+ end
+
+ test "compare case-insensitively" do
+ assert_operator ActiveStorage::Filename.new("foobar.pdf"), :==, ActiveStorage::Filename.new("FooBar.PDF")
+ end
+
+ test "compare sanitized" do
+ assert_operator ActiveStorage::Filename.new("foo-bar.pdf"), :==, ActiveStorage::Filename.new("foo\tbar.pdf")
+ end
+end
diff --git a/activestorage/test/models/preview_test.rb b/activestorage/test/models/preview_test.rb
new file mode 100644
index 0000000000..bcd8442f4b
--- /dev/null
+++ b/activestorage/test/models/preview_test.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::PreviewTest < ActiveSupport::TestCase
+ test "previewing a PDF" do
+ blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf")
+ preview = blob.preview(resize: "640x280").processed
+
+ assert preview.image.attached?
+ assert_equal "report.png", preview.image.filename.to_s
+ assert_equal "image/png", preview.image.content_type
+
+ image = read_image(preview.image)
+ assert_equal 612, image.width
+ assert_equal 792, image.height
+ end
+
+ test "previewing an MP4 video" do
+ blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4")
+ preview = blob.preview(resize: "640x280").processed
+
+ assert preview.image.attached?
+ assert_equal "video.png", preview.image.filename.to_s
+ assert_equal "image/png", preview.image.content_type
+
+ image = read_image(preview.image)
+ assert_equal 640, image.width
+ assert_equal 480, image.height
+ end
+
+ test "previewing an unpreviewable blob" do
+ blob = create_file_blob
+
+ assert_raises ActiveStorage::Blob::UnpreviewableError do
+ blob.preview resize: "640x280"
+ end
+ end
+end
diff --git a/activestorage/test/models/representation_test.rb b/activestorage/test/models/representation_test.rb
new file mode 100644
index 0000000000..29fe61aee4
--- /dev/null
+++ b/activestorage/test/models/representation_test.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::RepresentationTest < ActiveSupport::TestCase
+ test "representing an image" do
+ blob = create_file_blob
+ representation = blob.representation(resize: "100x100").processed
+
+ image = read_image(representation.image)
+ assert_equal 100, image.width
+ assert_equal 67, image.height
+ end
+
+ test "representing a PDF" do
+ blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf")
+ representation = blob.representation(resize: "640x280").processed
+
+ image = read_image(representation.image)
+ assert_equal 612, image.width
+ assert_equal 792, image.height
+ end
+
+ test "representing an MP4 video" do
+ blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4")
+ representation = blob.representation(resize: "640x280").processed
+
+ image = read_image(representation.image)
+ assert_equal 640, image.width
+ assert_equal 480, image.height
+ end
+
+ test "representing an unrepresentable blob" do
+ blob = create_blob
+
+ assert_raises ActiveStorage::Blob::UnrepresentableError do
+ blob.representation resize: "100x100"
+ end
+ end
+end
diff --git a/activestorage/test/models/variant_test.rb b/activestorage/test/models/variant_test.rb
new file mode 100644
index 0000000000..7157bfac3a
--- /dev/null
+++ b/activestorage/test/models/variant_test.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::VariantTest < ActiveSupport::TestCase
+ test "resized variation of JPEG blob" do
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = blob.variant(resize: "100x100").processed
+ assert_match(/racecar\.jpg/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal 100, image.width
+ assert_equal 67, image.height
+ end
+
+ test "resized and monochrome variation of JPEG blob" do
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = blob.variant(resize: "100x100", monochrome: true).processed
+ assert_match(/racecar\.jpg/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal 100, image.width
+ assert_equal 67, image.height
+ assert_match(/Gray/, image.colorspace)
+ end
+
+ test "center-weighted crop of JPEG blob" do
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = blob.variant(combine_options: {
+ gravity: "center",
+ resize: "100x100^",
+ crop: "100x100+0+0",
+ }).processed
+ assert_match(/racecar\.jpg/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal 100, image.width
+ assert_equal 100, image.height
+ end
+
+ test "resized variation of PSD blob" do
+ blob = create_file_blob(filename: "icon.psd", content_type: "image/vnd.adobe.photoshop")
+ variant = blob.variant(resize: "20x20").processed
+ assert_match(/icon\.png/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal "PNG", image.type
+ assert_equal 20, image.width
+ assert_equal 20, image.height
+ end
+
+ test "optimized variation of GIF blob" do
+ blob = create_file_blob(filename: "image.gif", content_type: "image/gif")
+
+ assert_nothing_raised do
+ blob.variant(layers: "Optimize").processed
+ end
+ end
+
+ test "variation of invariable blob" do
+ assert_raises ActiveStorage::Blob::InvariableError do
+ create_file_blob(filename: "report.pdf", content_type: "application/pdf").variant(resize: "100x100")
+ end
+ end
+
+ test "service_url doesn't grow in length despite long variant options" do
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = blob.variant(font: "a" * 10_000).processed
+ assert_operator variant.service_url.length, :<, 500
+ end
+end
diff --git a/activestorage/test/previewer/pdf_previewer_test.rb b/activestorage/test/previewer/pdf_previewer_test.rb
new file mode 100644
index 0000000000..fe32f39be4
--- /dev/null
+++ b/activestorage/test/previewer/pdf_previewer_test.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+require "active_storage/previewer/pdf_previewer"
+
+class ActiveStorage::Previewer::PDFPreviewerTest < ActiveSupport::TestCase
+ setup do
+ @blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf")
+ end
+
+ test "previewing a PDF document" do
+ ActiveStorage::Previewer::PDFPreviewer.new(@blob).preview do |attachable|
+ assert_equal "image/png", attachable[:content_type]
+ assert_equal "report.png", attachable[:filename]
+
+ image = MiniMagick::Image.read(attachable[:io])
+ assert_equal 612, image.width
+ assert_equal 792, image.height
+ end
+ end
+end
diff --git a/activestorage/test/previewer/video_previewer_test.rb b/activestorage/test/previewer/video_previewer_test.rb
new file mode 100644
index 0000000000..dba9b0d7e2
--- /dev/null
+++ b/activestorage/test/previewer/video_previewer_test.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+require "active_storage/previewer/video_previewer"
+
+class ActiveStorage::Previewer::VideoPreviewerTest < ActiveSupport::TestCase
+ setup do
+ @blob = create_file_blob(filename: "video.mp4", content_type: "video/mp4")
+ end
+
+ test "previewing an MP4 video" do
+ ActiveStorage::Previewer::VideoPreviewer.new(@blob).preview do |attachable|
+ assert_equal "image/png", attachable[:content_type]
+ assert_equal "video.png", attachable[:filename]
+
+ image = MiniMagick::Image.read(attachable[:io])
+ assert_equal 640, image.width
+ assert_equal 480, image.height
+ end
+ end
+end
diff --git a/activestorage/test/service/azure_storage_service_test.rb b/activestorage/test/service/azure_storage_service_test.rb
new file mode 100644
index 0000000000..be31bbe858
--- /dev/null
+++ b/activestorage/test/service/azure_storage_service_test.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "service/shared_service_tests"
+require "uri"
+
+if SERVICE_CONFIGURATIONS[:azure]
+ class ActiveStorage::Service::AzureStorageServiceTest < ActiveSupport::TestCase
+ SERVICE = ActiveStorage::Service.configure(:azure, SERVICE_CONFIGURATIONS)
+
+ include ActiveStorage::Service::SharedServiceTests
+
+ test "signed URL generation" do
+ url = @service.url(FIXTURE_KEY, expires_in: 5.minutes,
+ disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png")
+
+ assert_match(/(\S+)&rscd=inline%3B\+filename%3D%22avatar\.png%22%3B\+filename\*%3DUTF-8%27%27avatar\.png&rsct=image%2Fpng/, url)
+ assert_match SERVICE_CONFIGURATIONS[:azure][:container], url
+ end
+ end
+else
+ puts "Skipping Azure Storage Service tests because no Azure configuration was supplied"
+end
diff --git a/activestorage/test/service/configurations.example.yml b/activestorage/test/service/configurations.example.yml
new file mode 100644
index 0000000000..43cc013bc8
--- /dev/null
+++ b/activestorage/test/service/configurations.example.yml
@@ -0,0 +1,30 @@
+# s3:
+# service: S3
+# access_key_id: ""
+# secret_access_key: ""
+# region: ""
+# bucket: ""
+#
+# gcs:
+# service: GCS
+# credentials: {
+# type: "service_account",
+# project_id: "",
+# private_key_id: "",
+# private_key: "",
+# client_email: "",
+# client_id: "",
+# auth_uri: "https://accounts.google.com/o/oauth2/auth",
+# token_uri: "https://accounts.google.com/o/oauth2/token",
+# auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs",
+# client_x509_cert_url: ""
+# }
+# project:
+# bucket:
+#
+# azure:
+# service: AzureStorage
+# path: ""
+# storage_account_name: ""
+# storage_access_key: ""
+# container: ""
diff --git a/activestorage/test/service/configurations.yml.enc b/activestorage/test/service/configurations.yml.enc
new file mode 100644
index 0000000000..df11aac161
--- /dev/null
+++ b/activestorage/test/service/configurations.yml.enc
Binary files differ
diff --git a/activestorage/test/service/configurator_test.rb b/activestorage/test/service/configurator_test.rb
new file mode 100644
index 0000000000..a2fd035e02
--- /dev/null
+++ b/activestorage/test/service/configurator_test.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require "service/shared_service_tests"
+
+class ActiveStorage::Service::ConfiguratorTest < ActiveSupport::TestCase
+ test "builds correct service instance based on service name" do
+ service = ActiveStorage::Service::Configurator.build(:foo, foo: { service: "Disk", root: "path" })
+ assert_instance_of ActiveStorage::Service::DiskService, service
+ end
+
+ test "raises error when passing non-existent service name" do
+ assert_raise RuntimeError do
+ ActiveStorage::Service::Configurator.build(:bigfoot, {})
+ end
+ end
+end
diff --git a/activestorage/test/service/disk_service_test.rb b/activestorage/test/service/disk_service_test.rb
new file mode 100644
index 0000000000..4a6361b920
--- /dev/null
+++ b/activestorage/test/service/disk_service_test.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require "service/shared_service_tests"
+
+class ActiveStorage::Service::DiskServiceTest < ActiveSupport::TestCase
+ SERVICE = ActiveStorage::Service::DiskService.new(root: File.join(Dir.tmpdir, "active_storage"))
+
+ include ActiveStorage::Service::SharedServiceTests
+
+ test "url generation" do
+ assert_match(/rails\/active_storage\/disk\/.*\/avatar\.png\?content_type=image%2Fpng&disposition=inline/,
+ @service.url(FIXTURE_KEY, expires_in: 5.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png"))
+ end
+end
diff --git a/activestorage/test/service/gcs_service_test.rb b/activestorage/test/service/gcs_service_test.rb
new file mode 100644
index 0000000000..7efcd60fb7
--- /dev/null
+++ b/activestorage/test/service/gcs_service_test.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require "service/shared_service_tests"
+require "net/http"
+
+if SERVICE_CONFIGURATIONS[:gcs]
+ class ActiveStorage::Service::GCSServiceTest < ActiveSupport::TestCase
+ SERVICE = ActiveStorage::Service.configure(:gcs, SERVICE_CONFIGURATIONS)
+
+ include ActiveStorage::Service::SharedServiceTests
+
+ test "direct upload" do
+ begin
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ checksum = Digest::MD5.base64digest(data)
+ url = @service.url_for_direct_upload(key, expires_in: 5.minutes, content_type: "text/plain", content_length: data.size, checksum: checksum)
+
+ uri = URI.parse url
+ request = Net::HTTP::Put.new uri.request_uri
+ request.body = data
+ request.add_field "Content-Type", "text/plain"
+ request.add_field "Content-MD5", checksum
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
+ http.request request
+ end
+
+ assert_equal data, @service.download(key)
+ ensure
+ @service.delete key
+ end
+ end
+
+ test "signed URL generation" do
+ assert_match(/storage\.googleapis\.com\/.*response-content-disposition=inline.*test\.txt.*response-content-type=text%2Fplain/,
+ @service.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain"))
+ end
+
+ test "signed URL response headers" do
+ begin
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ @service.upload(key, StringIO.new(data), checksum: Digest::MD5.base64digest(data))
+
+ url = @service.url(key, expires_in: 2.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain")
+ response = Net::HTTP.get_response(URI(url))
+ assert_equal "text/plain", response.header["Content-Type"]
+ ensure
+ @service.delete key
+ end
+ end
+ end
+else
+ puts "Skipping GCS Service tests because no GCS configuration was supplied"
+end
diff --git a/activestorage/test/service/mirror_service_test.rb b/activestorage/test/service/mirror_service_test.rb
new file mode 100644
index 0000000000..92101b1282
--- /dev/null
+++ b/activestorage/test/service/mirror_service_test.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require "service/shared_service_tests"
+
+class ActiveStorage::Service::MirrorServiceTest < ActiveSupport::TestCase
+ mirror_config = (1..3).map do |i|
+ [ "mirror_#{i}",
+ service: "Disk",
+ root: Dir.mktmpdir("active_storage_tests_mirror_#{i}") ]
+ end.to_h
+
+ config = mirror_config.merge \
+ mirror: { service: "Mirror", primary: "primary", mirrors: mirror_config.keys },
+ primary: { service: "Disk", root: Dir.mktmpdir("active_storage_tests_primary") }
+
+ SERVICE = ActiveStorage::Service.configure :mirror, config
+
+ include ActiveStorage::Service::SharedServiceTests
+
+ test "uploading to all services" do
+ begin
+ data = "Something else entirely!"
+ key = upload(data, to: @service)
+
+ assert_equal data, SERVICE.primary.download(key)
+ SERVICE.mirrors.each do |mirror|
+ assert_equal data, mirror.download(key)
+ end
+ ensure
+ @service.delete key
+ end
+ end
+
+ test "downloading from primary service" do
+ data = "Something else entirely!"
+ key = upload(data, to: SERVICE.primary)
+
+ assert_equal data, @service.download(key)
+ end
+
+ test "deleting from all services" do
+ @service.delete FIXTURE_KEY
+ assert_not SERVICE.primary.exist?(FIXTURE_KEY)
+ SERVICE.mirrors.each do |mirror|
+ assert_not mirror.exist?(FIXTURE_KEY)
+ end
+ end
+
+ test "URL generation in primary service" do
+ filename = ActiveStorage::Filename.new("test.txt")
+
+ freeze_time do
+ assert_equal SERVICE.primary.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: filename, content_type: "text/plain"),
+ @service.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: filename, content_type: "text/plain")
+ end
+ end
+
+ private
+ def upload(data, to:)
+ SecureRandom.base58(24).tap do |key|
+ io = StringIO.new(data).tap(&:read)
+ @service.upload key, io, checksum: Digest::MD5.base64digest(data)
+ assert io.eof?
+ end
+ end
+end
diff --git a/activestorage/test/service/s3_service_test.rb b/activestorage/test/service/s3_service_test.rb
new file mode 100644
index 0000000000..c3818422aa
--- /dev/null
+++ b/activestorage/test/service/s3_service_test.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require "service/shared_service_tests"
+require "net/http"
+
+if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].present?
+ class ActiveStorage::Service::S3ServiceTest < ActiveSupport::TestCase
+ SERVICE = ActiveStorage::Service.configure(:s3, SERVICE_CONFIGURATIONS)
+
+ include ActiveStorage::Service::SharedServiceTests
+
+ test "direct upload" do
+ begin
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ checksum = Digest::MD5.base64digest(data)
+ url = @service.url_for_direct_upload(key, expires_in: 5.minutes, content_type: "text/plain", content_length: data.size, checksum: checksum)
+
+ uri = URI.parse url
+ request = Net::HTTP::Put.new uri.request_uri
+ request.body = data
+ request.add_field "Content-Type", "text/plain"
+ request.add_field "Content-MD5", checksum
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
+ http.request request
+ end
+
+ assert_equal data, @service.download(key)
+ ensure
+ @service.delete key
+ end
+ end
+
+ test "signed URL generation" do
+ url = @service.url(FIXTURE_KEY, expires_in: 5.minutes,
+ disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png")
+
+ assert_match(/s3\.(\S+)?amazonaws.com.*response-content-disposition=inline.*avatar\.png.*response-content-type=image%2Fpng/, url)
+ assert_match SERVICE_CONFIGURATIONS[:s3][:bucket], url
+ end
+
+ test "uploading with server-side encryption" do
+ config = SERVICE_CONFIGURATIONS.deep_merge(s3: { upload: { server_side_encryption: "AES256" } })
+ service = ActiveStorage::Service.configure(:s3, config)
+
+ begin
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ service.upload key, StringIO.new(data), checksum: Digest::MD5.base64digest(data)
+
+ assert_equal "AES256", service.bucket.object(key).server_side_encryption
+ ensure
+ service.delete key
+ end
+ end
+ end
+else
+ puts "Skipping S3 Service tests because no S3 configuration was supplied"
+end
diff --git a/activestorage/test/service/shared_service_tests.rb b/activestorage/test/service/shared_service_tests.rb
new file mode 100644
index 0000000000..ce28c4393a
--- /dev/null
+++ b/activestorage/test/service/shared_service_tests.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "active_support/core_ext/securerandom"
+
+module ActiveStorage::Service::SharedServiceTests
+ extend ActiveSupport::Concern
+
+ FIXTURE_KEY = SecureRandom.base58(24)
+ FIXTURE_DATA = "\211PNG\r\n\032\n\000\000\000\rIHDR\000\000\000\020\000\000\000\020\001\003\000\000\000%=m\"\000\000\000\006PLTE\000\000\000\377\377\377\245\331\237\335\000\000\0003IDATx\234c\370\377\237\341\377_\206\377\237\031\016\2603\334?\314p\1772\303\315\315\f7\215\031\356\024\203\320\275\317\f\367\201R\314\f\017\300\350\377\177\000Q\206\027(\316]\233P\000\000\000\000IEND\256B`\202".dup.force_encoding(Encoding::BINARY)
+
+ included do
+ setup do
+ @service = self.class.const_get(:SERVICE)
+ @service.upload FIXTURE_KEY, StringIO.new(FIXTURE_DATA)
+ end
+
+ teardown do
+ @service.delete FIXTURE_KEY
+ end
+
+ test "uploading with integrity" do
+ begin
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ @service.upload(key, StringIO.new(data), checksum: Digest::MD5.base64digest(data))
+
+ assert_equal data, @service.download(key)
+ ensure
+ @service.delete key
+ end
+ end
+
+ test "uploading without integrity" do
+ begin
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+
+ assert_raises(ActiveStorage::IntegrityError) do
+ @service.upload(key, StringIO.new(data), checksum: Digest::MD5.base64digest("bad data"))
+ end
+
+ assert_not @service.exist?(key)
+ ensure
+ @service.delete key
+ end
+ end
+
+ test "downloading" do
+ assert_equal FIXTURE_DATA, @service.download(FIXTURE_KEY)
+ end
+
+ test "downloading in chunks" do
+ chunks = []
+
+ @service.download(FIXTURE_KEY) do |chunk|
+ chunks << chunk
+ end
+
+ assert_equal [ FIXTURE_DATA ], chunks
+ end
+
+ test "existing" do
+ assert @service.exist?(FIXTURE_KEY)
+ assert_not @service.exist?(FIXTURE_KEY + "nonsense")
+ end
+
+ test "deleting" do
+ @service.delete FIXTURE_KEY
+ assert_not @service.exist?(FIXTURE_KEY)
+ end
+
+ test "deleting nonexistent key" do
+ assert_nothing_raised do
+ @service.delete SecureRandom.base58(24)
+ end
+ end
+
+ test "deleting by prefix" do
+ begin
+ @service.upload("a/a/a", StringIO.new(FIXTURE_DATA))
+ @service.upload("a/a/b", StringIO.new(FIXTURE_DATA))
+ @service.upload("a/b/a", StringIO.new(FIXTURE_DATA))
+
+ @service.delete_prefixed("a/a/")
+ assert_not @service.exist?("a/a/a")
+ assert_not @service.exist?("a/a/b")
+ assert @service.exist?("a/b/a")
+ ensure
+ @service.delete("a/a/a")
+ @service.delete("a/a/b")
+ @service.delete("a/b/a")
+ end
+ end
+ end
+end
diff --git a/activestorage/test/template/image_tag_test.rb b/activestorage/test/template/image_tag_test.rb
new file mode 100644
index 0000000000..80f183e0e3
--- /dev/null
+++ b/activestorage/test/template/image_tag_test.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::ImageTagTest < ActionView::TestCase
+ tests ActionView::Helpers::AssetTagHelper
+
+ setup do
+ @blob = create_file_blob filename: "racecar.jpg"
+ end
+
+ test "blob" do
+ assert_dom_equal %(<img src="#{polymorphic_url @blob}" />), image_tag(@blob)
+ end
+
+ test "variant" do
+ variant = @blob.variant(resize: "100x100")
+ assert_dom_equal %(<img src="#{polymorphic_url variant}" />), image_tag(variant)
+ end
+
+ test "preview" do
+ blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf")
+ preview = blob.preview(resize: "100x100")
+ assert_dom_equal %(<img src="#{polymorphic_url preview}" />), image_tag(preview)
+ end
+
+ test "attachment" do
+ attachment = ActiveStorage::Attachment.new(blob: @blob)
+ assert_dom_equal %(<img src="#{polymorphic_url attachment}" />), image_tag(attachment)
+ end
+
+ test "error when attachment's empty" do
+ @user = User.create!(name: "DHH")
+
+ assert_not @user.avatar.attached?
+ assert_raises(ArgumentError) { image_tag(@user.avatar) }
+ end
+
+ test "error when object can't be resolved into url" do
+ unresolvable_object = ActionView::Helpers::AssetTagHelper
+ assert_raises(ArgumentError) { image_tag(unresolvable_object) }
+ end
+end
diff --git a/activestorage/test/test_helper.rb b/activestorage/test/test_helper.rb
new file mode 100644
index 0000000000..aaf1d452ea
--- /dev/null
+++ b/activestorage/test/test_helper.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+ENV["RAILS_ENV"] ||= "test"
+require_relative "dummy/config/environment.rb"
+
+require "bundler/setup"
+require "active_support"
+require "active_support/test_case"
+require "active_support/testing/autorun"
+require "mini_magick"
+
+begin
+ require "byebug"
+rescue LoadError
+end
+
+require "active_job"
+ActiveJob::Base.queue_adapter = :test
+ActiveJob::Base.logger = ActiveSupport::Logger.new(nil)
+
+# Filter out Minitest backtrace while allowing backtrace from other libraries
+# to be shown.
+Minitest.backtrace_filter = Minitest::BacktraceFilter.new
+
+require "yaml"
+SERVICE_CONFIGURATIONS = begin
+ erb = ERB.new(Pathname.new(File.expand_path("service/configurations.yml", __dir__)).read)
+ configuration = YAML.load(erb.result) || {}
+ configuration.deep_symbolize_keys
+rescue Errno::ENOENT
+ puts "Missing service configuration file in test/service/configurations.yml"
+ {}
+end
+
+require "tmpdir"
+ActiveStorage::Blob.service = ActiveStorage::Service::DiskService.new(root: Dir.mktmpdir("active_storage_tests"))
+
+ActiveStorage.logger = ActiveSupport::Logger.new(nil)
+ActiveStorage.verifier = ActiveSupport::MessageVerifier.new("Testing")
+
+class ActiveSupport::TestCase
+ self.file_fixture_path = File.expand_path("fixtures/files", __dir__)
+
+ private
+ def create_blob(data: "Hello world!", filename: "hello.txt", content_type: "text/plain")
+ ActiveStorage::Blob.create_after_upload! io: StringIO.new(data), filename: filename, content_type: content_type
+ end
+
+ def create_file_blob(filename: "racecar.jpg", content_type: "image/jpeg", metadata: nil)
+ ActiveStorage::Blob.create_after_upload! io: file_fixture(filename).open, filename: filename, content_type: content_type, metadata: metadata
+ end
+
+ def create_blob_before_direct_upload(filename: "hello.txt", byte_size:, checksum:, content_type: "text/plain")
+ ActiveStorage::Blob.create_before_direct_upload! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type
+ end
+
+ def read_image(blob_or_variant)
+ MiniMagick::Image.open blob_or_variant.service.send(:path_for, blob_or_variant.key)
+ end
+end
+
+require "global_id"
+GlobalID.app = "ActiveStorageExampleApp"
+ActiveRecord::Base.send :include, GlobalID::Identification
+
+class User < ActiveRecord::Base
+ has_one_attached :avatar
+ has_many_attached :highlights
+end
diff --git a/activestorage/webpack.config.js b/activestorage/webpack.config.js
new file mode 100644
index 0000000000..3a50eef470
--- /dev/null
+++ b/activestorage/webpack.config.js
@@ -0,0 +1,26 @@
+const path = require("path")
+
+module.exports = {
+ entry: {
+ "activestorage": path.resolve(__dirname, "app/javascript/activestorage/index.js"),
+ },
+
+ output: {
+ filename: "[name].js",
+ path: path.resolve(__dirname, "app/assets/javascripts"),
+ library: "ActiveStorage",
+ libraryTarget: "umd"
+ },
+
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ use: {
+ loader: "babel-loader"
+ }
+ }
+ ]
+ }
+}
diff --git a/activestorage/yarn.lock b/activestorage/yarn.lock
new file mode 100644
index 0000000000..41742be201
--- /dev/null
+++ b/activestorage/yarn.lock
@@ -0,0 +1,3154 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+abbrev@1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f"
+
+acorn-dynamic-import@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4"
+ dependencies:
+ acorn "^4.0.3"
+
+acorn-jsx@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
+ dependencies:
+ acorn "^3.0.4"
+
+acorn@^3.0.4:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
+
+acorn@^4.0.3:
+ version "4.0.13"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
+
+acorn@^5.0.0, acorn@^5.0.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.1.tgz#53fe161111f912ab999ee887a90a0bc52822fd75"
+
+ajv-keywords@^1.0.0:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
+
+ajv-keywords@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0"
+
+ajv@^4.7.0, ajv@^4.9.1:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
+ dependencies:
+ co "^4.6.0"
+ json-stable-stringify "^1.0.1"
+
+ajv@^5.1.5, ajv@^5.2.0:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
+ dependencies:
+ co "^4.6.0"
+ fast-deep-equal "^1.0.0"
+ json-schema-traverse "^0.3.0"
+ json-stable-stringify "^1.0.1"
+
+align-text@^0.1.1, align-text@^0.1.3:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
+ dependencies:
+ kind-of "^3.0.2"
+ longest "^1.0.1"
+ repeat-string "^1.5.2"
+
+ansi-escapes@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-2.0.0.tgz#5bae52be424878dd9783e8910e3fc2922e83c81b"
+
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+
+ansi-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+
+ansi-styles@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+
+ansi-styles@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88"
+ dependencies:
+ color-convert "^1.9.0"
+
+anymatch@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507"
+ dependencies:
+ arrify "^1.0.0"
+ micromatch "^2.1.5"
+
+aproba@^1.0.3:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.2.tgz#45c6629094de4e96f693ef7eab74ae079c240fc1"
+
+are-we-there-yet@~1.1.2:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^2.0.6"
+
+argparse@^1.0.7:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86"
+ dependencies:
+ sprintf-js "~1.0.2"
+
+arr-diff@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
+ dependencies:
+ arr-flatten "^1.0.1"
+
+arr-flatten@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+
+array-union@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
+ dependencies:
+ array-uniq "^1.0.1"
+
+array-uniq@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
+
+array-unique@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
+
+arrify@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+
+asn1.js@^4.0.0:
+ version "4.9.1"
+ resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40"
+ dependencies:
+ bn.js "^4.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+asn1@~0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+
+assert-plus@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
+
+assert@^1.1.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
+ dependencies:
+ util "0.10.3"
+
+async-each@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
+
+async@^2.1.2:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
+ dependencies:
+ lodash "^4.14.0"
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+
+aws-sign2@~0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
+
+aws4@^1.2.1:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
+
+babel-code-frame@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4"
+ dependencies:
+ chalk "^1.1.0"
+ esutils "^2.0.2"
+ js-tokens "^3.0.0"
+
+babel-core@^6.24.1, babel-core@^6.25.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.25.0.tgz#7dd42b0463c742e9d5296deb3ec67a9322dad729"
+ dependencies:
+ babel-code-frame "^6.22.0"
+ babel-generator "^6.25.0"
+ babel-helpers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-register "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.25.0"
+ babel-traverse "^6.25.0"
+ babel-types "^6.25.0"
+ babylon "^6.17.2"
+ convert-source-map "^1.1.0"
+ debug "^2.1.1"
+ json5 "^0.5.0"
+ lodash "^4.2.0"
+ minimatch "^3.0.2"
+ path-is-absolute "^1.0.0"
+ private "^0.1.6"
+ slash "^1.0.0"
+ source-map "^0.5.0"
+
+babel-generator@^6.25.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.25.0.tgz#33a1af70d5f2890aeb465a4a7793c1df6a9ea9fc"
+ dependencies:
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-types "^6.25.0"
+ detect-indent "^4.0.0"
+ jsesc "^1.3.0"
+ lodash "^4.2.0"
+ source-map "^0.5.0"
+ trim-right "^1.0.1"
+
+babel-helper-builder-binary-assignment-operator-visitor@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664"
+ dependencies:
+ babel-helper-explode-assignable-expression "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-call-delegate@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d"
+ dependencies:
+ babel-helper-hoist-variables "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-define-map@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.24.1.tgz#7a9747f258d8947d32d515f6aa1c7bd02204a080"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+ lodash "^4.2.0"
+
+babel-helper-explode-assignable-expression@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-function-name@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9"
+ dependencies:
+ babel-helper-get-function-arity "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-get-function-arity@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-hoist-variables@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-optimise-call-expression@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-regex@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.24.1.tgz#d36e22fab1008d79d88648e32116868128456ce8"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+ lodash "^4.2.0"
+
+babel-helper-remap-async-to-generator@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-replace-supers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a"
+ dependencies:
+ babel-helper-optimise-call-expression "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helpers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-loader@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.1.tgz#b87134c8b12e3e4c2a94e0546085bc680a2b8488"
+ dependencies:
+ find-cache-dir "^1.0.0"
+ loader-utils "^1.0.2"
+ mkdirp "^0.5.1"
+
+babel-messages@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-check-es2015-constants@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-syntax-async-functions@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95"
+
+babel-plugin-syntax-exponentiation-operator@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
+
+babel-plugin-syntax-trailing-function-commas@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3"
+
+babel-plugin-transform-async-to-generator@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761"
+ dependencies:
+ babel-helper-remap-async-to-generator "^6.24.1"
+ babel-plugin-syntax-async-functions "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-arrow-functions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoped-functions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoping@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.24.1.tgz#76c295dc3a4741b1665adfd3167215dcff32a576"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+ lodash "^4.2.0"
+
+babel-plugin-transform-es2015-classes@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db"
+ dependencies:
+ babel-helper-define-map "^6.24.1"
+ babel-helper-function-name "^6.24.1"
+ babel-helper-optimise-call-expression "^6.24.1"
+ babel-helper-replace-supers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-computed-properties@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-destructuring@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-duplicate-keys@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-for-of@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-function-name@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-literals@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154"
+ dependencies:
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.1.tgz#d3e310b40ef664a36622200097c6d440298f2bfe"
+ dependencies:
+ babel-plugin-transform-strict-mode "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-modules-systemjs@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23"
+ dependencies:
+ babel-helper-hoist-variables "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-modules-umd@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468"
+ dependencies:
+ babel-plugin-transform-es2015-modules-amd "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-object-super@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d"
+ dependencies:
+ babel-helper-replace-supers "^6.24.1"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-parameters@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b"
+ dependencies:
+ babel-helper-call-delegate "^6.24.1"
+ babel-helper-get-function-arity "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-shorthand-properties@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-spread@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-sticky-regex@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc"
+ dependencies:
+ babel-helper-regex "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-template-literals@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-typeof-symbol@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-unicode-regex@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9"
+ dependencies:
+ babel-helper-regex "^6.24.1"
+ babel-runtime "^6.22.0"
+ regexpu-core "^2.0.0"
+
+babel-plugin-transform-exponentiation-operator@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e"
+ dependencies:
+ babel-helper-builder-binary-assignment-operator-visitor "^6.24.1"
+ babel-plugin-syntax-exponentiation-operator "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-regenerator@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.24.1.tgz#b8da305ad43c3c99b4848e4fe4037b770d23c418"
+ dependencies:
+ regenerator-transform "0.9.11"
+
+babel-plugin-transform-strict-mode@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-preset-env@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.0.tgz#2de1c782a780a0a5d605d199c957596da43c44e4"
+ dependencies:
+ babel-plugin-check-es2015-constants "^6.22.0"
+ babel-plugin-syntax-trailing-function-commas "^6.22.0"
+ babel-plugin-transform-async-to-generator "^6.22.0"
+ babel-plugin-transform-es2015-arrow-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoped-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoping "^6.23.0"
+ babel-plugin-transform-es2015-classes "^6.23.0"
+ babel-plugin-transform-es2015-computed-properties "^6.22.0"
+ babel-plugin-transform-es2015-destructuring "^6.23.0"
+ babel-plugin-transform-es2015-duplicate-keys "^6.22.0"
+ babel-plugin-transform-es2015-for-of "^6.23.0"
+ babel-plugin-transform-es2015-function-name "^6.22.0"
+ babel-plugin-transform-es2015-literals "^6.22.0"
+ babel-plugin-transform-es2015-modules-amd "^6.22.0"
+ babel-plugin-transform-es2015-modules-commonjs "^6.23.0"
+ babel-plugin-transform-es2015-modules-systemjs "^6.23.0"
+ babel-plugin-transform-es2015-modules-umd "^6.23.0"
+ babel-plugin-transform-es2015-object-super "^6.22.0"
+ babel-plugin-transform-es2015-parameters "^6.23.0"
+ babel-plugin-transform-es2015-shorthand-properties "^6.22.0"
+ babel-plugin-transform-es2015-spread "^6.22.0"
+ babel-plugin-transform-es2015-sticky-regex "^6.22.0"
+ babel-plugin-transform-es2015-template-literals "^6.22.0"
+ babel-plugin-transform-es2015-typeof-symbol "^6.23.0"
+ babel-plugin-transform-es2015-unicode-regex "^6.22.0"
+ babel-plugin-transform-exponentiation-operator "^6.22.0"
+ babel-plugin-transform-regenerator "^6.22.0"
+ browserslist "^2.1.2"
+ invariant "^2.2.2"
+ semver "^5.3.0"
+
+babel-register@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.24.1.tgz#7e10e13a2f71065bdfad5a1787ba45bca6ded75f"
+ dependencies:
+ babel-core "^6.24.1"
+ babel-runtime "^6.22.0"
+ core-js "^2.4.0"
+ home-or-tmp "^2.0.0"
+ lodash "^4.2.0"
+ mkdirp "^0.5.1"
+ source-map-support "^0.4.2"
+
+babel-runtime@^6.18.0, babel-runtime@^6.22.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b"
+ dependencies:
+ core-js "^2.4.0"
+ regenerator-runtime "^0.10.0"
+
+babel-template@^6.24.1, babel-template@^6.25.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.25.0.tgz#665241166b7c2aa4c619d71e192969552b10c071"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.25.0"
+ babel-types "^6.25.0"
+ babylon "^6.17.2"
+ lodash "^4.2.0"
+
+babel-traverse@^6.24.1, babel-traverse@^6.25.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.25.0.tgz#2257497e2fcd19b89edc13c4c91381f9512496f1"
+ dependencies:
+ babel-code-frame "^6.22.0"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-types "^6.25.0"
+ babylon "^6.17.2"
+ debug "^2.2.0"
+ globals "^9.0.0"
+ invariant "^2.2.0"
+ lodash "^4.2.0"
+
+babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.25.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.25.0.tgz#70afb248d5660e5d18f811d91c8303b54134a18e"
+ dependencies:
+ babel-runtime "^6.22.0"
+ esutils "^2.0.2"
+ lodash "^4.2.0"
+ to-fast-properties "^1.0.1"
+
+babylon@^6.17.2:
+ version "6.17.4"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.4.tgz#3e8b7402b88d22c3423e137a1577883b15ff869a"
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+
+base64-js@^1.0.2:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886"
+
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
+ dependencies:
+ tweetnacl "^0.14.3"
+
+big.js@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.1.3.tgz#4cada2193652eb3ca9ec8e55c9015669c9806978"
+
+binary-extensions@^1.0.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.9.0.tgz#66506c16ce6f4d6928a5b3cd6a33ca41e941e37b"
+
+block-stream@*:
+ version "0.0.9"
+ resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
+ dependencies:
+ inherits "~2.0.0"
+
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
+ version "4.11.7"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.7.tgz#ddb048e50d9482790094c13eb3fcfc833ce7ab46"
+
+boom@2.x.x:
+ version "2.10.1"
+ resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
+ dependencies:
+ hoek "2.x.x"
+
+brace-expansion@^1.1.7:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^1.8.2:
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
+ dependencies:
+ expand-range "^1.8.1"
+ preserve "^0.2.0"
+ repeat-element "^1.1.2"
+
+brorand@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
+
+browserify-aes@^1.0.0, browserify-aes@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.0.6.tgz#5e7725dbdef1fd5930d4ebab48567ce451c48a0a"
+ dependencies:
+ buffer-xor "^1.0.2"
+ cipher-base "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.0"
+ inherits "^2.0.1"
+
+browserify-cipher@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a"
+ dependencies:
+ browserify-aes "^1.0.4"
+ browserify-des "^1.0.0"
+ evp_bytestokey "^1.0.0"
+
+browserify-des@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd"
+ dependencies:
+ cipher-base "^1.0.1"
+ des.js "^1.0.0"
+ inherits "^2.0.1"
+
+browserify-rsa@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
+ dependencies:
+ bn.js "^4.1.0"
+ randombytes "^2.0.1"
+
+browserify-sign@^4.0.0:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298"
+ dependencies:
+ bn.js "^4.1.1"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.2"
+ elliptic "^6.0.0"
+ inherits "^2.0.1"
+ parse-asn1 "^5.0.0"
+
+browserify-zlib@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d"
+ dependencies:
+ pako "~0.2.0"
+
+browserslist@^2.1.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.2.2.tgz#e9b4618b8a01c193f9786beea09f6fd10dbe31c3"
+ dependencies:
+ caniuse-lite "^1.0.30000704"
+ electron-to-chromium "^1.3.16"
+
+buffer-xor@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
+
+buffer@^4.3.0:
+ version "4.9.1"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298"
+ dependencies:
+ base64-js "^1.0.2"
+ ieee754 "^1.1.4"
+ isarray "^1.0.0"
+
+builtin-modules@^1.0.0, builtin-modules@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+
+builtin-status-codes@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
+
+caller-path@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
+ dependencies:
+ callsites "^0.2.0"
+
+callsites@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
+
+camelcase@^1.0.2:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
+
+camelcase@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
+
+caniuse-lite@^1.0.30000704:
+ version "1.0.30000706"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000706.tgz#bc59abc41ba7d4a3634dda95befded6114e1f24e"
+
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+
+center-align@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad"
+ dependencies:
+ align-text "^0.1.3"
+ lazy-cache "^1.0.3"
+
+chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
+ dependencies:
+ ansi-styles "^2.2.1"
+ escape-string-regexp "^1.0.2"
+ has-ansi "^2.0.0"
+ strip-ansi "^3.0.0"
+ supports-color "^2.0.0"
+
+chalk@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.0.1.tgz#dbec49436d2ae15f536114e76d14656cdbc0f44d"
+ dependencies:
+ ansi-styles "^3.1.0"
+ escape-string-regexp "^1.0.5"
+ supports-color "^4.0.0"
+
+chokidar@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
+ dependencies:
+ anymatch "^1.3.0"
+ async-each "^1.0.0"
+ glob-parent "^2.0.0"
+ inherits "^2.0.1"
+ is-binary-path "^1.0.0"
+ is-glob "^2.0.0"
+ path-is-absolute "^1.0.0"
+ readdirp "^2.0.0"
+ optionalDependencies:
+ fsevents "^1.0.0"
+
+cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+circular-json@^0.3.1:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
+
+cli-cursor@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
+ dependencies:
+ restore-cursor "^2.0.0"
+
+cli-width@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
+
+cliui@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
+ dependencies:
+ center-align "^0.1.1"
+ right-align "^0.1.1"
+ wordwrap "0.0.2"
+
+cliui@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wrap-ansi "^2.0.0"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+
+color-convert@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
+ dependencies:
+ color-name "^1.1.1"
+
+color-name@^1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+
+combined-stream@^1.0.5, combined-stream@~1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
+ dependencies:
+ delayed-stream "~1.0.0"
+
+commondir@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+
+concat-stream@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
+ dependencies:
+ inherits "^2.0.3"
+ readable-stream "^2.2.2"
+ typedarray "^0.0.6"
+
+console-browserify@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
+ dependencies:
+ date-now "^0.1.4"
+
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+
+constants-browserify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
+
+contains-path@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
+
+convert-source-map@^1.1.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5"
+
+core-js@^2.4.0:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
+
+core-util-is@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+
+create-ecdh@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
+ dependencies:
+ bn.js "^4.1.0"
+ elliptic "^6.0.0"
+
+create-hash@^1.1.0, create-hash@^1.1.1, create-hash@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd"
+ dependencies:
+ cipher-base "^1.0.1"
+ inherits "^2.0.1"
+ ripemd160 "^2.0.0"
+ sha.js "^2.4.0"
+
+create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06"
+ dependencies:
+ cipher-base "^1.0.3"
+ create-hash "^1.1.0"
+ inherits "^2.0.1"
+ ripemd160 "^2.0.0"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+cross-spawn@^5.0.1, cross-spawn@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
+ dependencies:
+ lru-cache "^4.0.1"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+cryptiles@2.x.x:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
+ dependencies:
+ boom "2.x.x"
+
+crypto-browserify@^3.11.0:
+ version "3.11.1"
+ resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f"
+ dependencies:
+ browserify-cipher "^1.0.0"
+ browserify-sign "^4.0.0"
+ create-ecdh "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.0"
+ diffie-hellman "^5.0.0"
+ inherits "^2.0.1"
+ pbkdf2 "^3.0.3"
+ public-encrypt "^4.0.0"
+ randombytes "^2.0.0"
+
+d@1:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
+ dependencies:
+ es5-ext "^0.10.9"
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ dependencies:
+ assert-plus "^1.0.0"
+
+date-now@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
+
+debug@^2.1.1, debug@^2.2.0, debug@^2.6.8:
+ version "2.6.8"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
+ dependencies:
+ ms "2.0.0"
+
+decamelize@^1.0.0, decamelize@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+
+deep-extend@~0.4.0:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
+
+deep-is@~0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+
+del@^2.0.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8"
+ dependencies:
+ globby "^5.0.0"
+ is-path-cwd "^1.0.0"
+ is-path-in-cwd "^1.0.0"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ rimraf "^2.2.8"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+
+delegates@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+
+des.js@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
+ dependencies:
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+detect-indent@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
+ dependencies:
+ repeating "^2.0.0"
+
+diffie-hellman@^5.0.0:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
+ dependencies:
+ bn.js "^4.1.0"
+ miller-rabin "^4.0.0"
+ randombytes "^2.0.0"
+
+doctrine@1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
+ dependencies:
+ esutils "^2.0.2"
+ isarray "^1.0.0"
+
+doctrine@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63"
+ dependencies:
+ esutils "^2.0.2"
+ isarray "^1.0.0"
+
+domain-browser@^1.1.1:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
+
+ecc-jsbn@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
+ dependencies:
+ jsbn "~0.1.0"
+
+electron-to-chromium@^1.3.16:
+ version "1.3.16"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.16.tgz#d0e026735754770901ae301a21664cba45d92f7d"
+
+elliptic@^6.0.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
+ dependencies:
+ bn.js "^4.4.0"
+ brorand "^1.0.1"
+ hash.js "^1.0.0"
+ hmac-drbg "^1.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.0"
+
+emojis-list@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
+
+enhanced-resolve@^3.4.0:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e"
+ dependencies:
+ graceful-fs "^4.1.2"
+ memory-fs "^0.4.0"
+ object-assign "^4.0.1"
+ tapable "^0.2.7"
+
+errno@^0.1.3:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
+ dependencies:
+ prr "~0.0.0"
+
+error-ex@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
+ dependencies:
+ is-arrayish "^0.2.1"
+
+es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14:
+ version "0.10.24"
+ resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.24.tgz#a55877c9924bc0c8d9bd3c2cbe17495ac1709b14"
+ dependencies:
+ es6-iterator "2"
+ es6-symbol "~3.1"
+
+es6-iterator@2, es6-iterator@^2.0.1, es6-iterator@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512"
+ dependencies:
+ d "1"
+ es5-ext "^0.10.14"
+ es6-symbol "^3.1"
+
+es6-map@^0.1.3:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0"
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+ es6-iterator "~2.0.1"
+ es6-set "~0.1.5"
+ es6-symbol "~3.1.1"
+ event-emitter "~0.3.5"
+
+es6-set@~0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+ es6-iterator "~2.0.1"
+ es6-symbol "3.1.1"
+ event-emitter "~0.3.5"
+
+es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1, es6-symbol@~3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+
+es6-weak-map@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f"
+ dependencies:
+ d "1"
+ es5-ext "^0.10.14"
+ es6-iterator "^2.0.1"
+ es6-symbol "^3.1.1"
+
+escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+
+escope@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3"
+ dependencies:
+ es6-map "^0.1.3"
+ es6-weak-map "^2.0.1"
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+eslint-import-resolver-node@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.1.tgz#4422574cde66a9a7b099938ee4d508a199e0e3cc"
+ dependencies:
+ debug "^2.6.8"
+ resolve "^1.2.0"
+
+eslint-module-utils@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz#abaec824177613b8a95b299639e1b6facf473449"
+ dependencies:
+ debug "^2.6.8"
+ pkg-dir "^1.0.0"
+
+eslint-plugin-import@^2.7.0:
+ version "2.7.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.7.0.tgz#21de33380b9efb55f5ef6d2e210ec0e07e7fa69f"
+ dependencies:
+ builtin-modules "^1.1.1"
+ contains-path "^0.1.0"
+ debug "^2.6.8"
+ doctrine "1.5.0"
+ eslint-import-resolver-node "^0.3.1"
+ eslint-module-utils "^2.1.1"
+ has "^1.0.1"
+ lodash.cond "^4.3.0"
+ minimatch "^3.0.3"
+ read-pkg-up "^2.0.0"
+
+eslint-scope@^3.7.1:
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
+ dependencies:
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+eslint@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.3.0.tgz#fcd7c96376bbf34c85ee67ed0012a299642b108f"
+ dependencies:
+ ajv "^5.2.0"
+ babel-code-frame "^6.22.0"
+ chalk "^1.1.3"
+ concat-stream "^1.6.0"
+ cross-spawn "^5.1.0"
+ debug "^2.6.8"
+ doctrine "^2.0.0"
+ eslint-scope "^3.7.1"
+ espree "^3.4.3"
+ esquery "^1.0.0"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ file-entry-cache "^2.0.0"
+ functional-red-black-tree "^1.0.1"
+ glob "^7.1.2"
+ globals "^9.17.0"
+ ignore "^3.3.3"
+ imurmurhash "^0.1.4"
+ inquirer "^3.0.6"
+ is-resolvable "^1.0.0"
+ js-yaml "^3.8.4"
+ json-stable-stringify "^1.0.1"
+ levn "^0.3.0"
+ lodash "^4.17.4"
+ minimatch "^3.0.2"
+ mkdirp "^0.5.1"
+ natural-compare "^1.4.0"
+ optionator "^0.8.2"
+ path-is-inside "^1.0.2"
+ pluralize "^4.0.0"
+ progress "^2.0.0"
+ require-uncached "^1.0.3"
+ semver "^5.3.0"
+ strip-json-comments "~2.0.1"
+ table "^4.0.1"
+ text-table "~0.2.0"
+
+espree@^3.4.3:
+ version "3.4.3"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-3.4.3.tgz#2910b5ccd49ce893c2ffffaab4fd8b3a31b82374"
+ dependencies:
+ acorn "^5.0.1"
+ acorn-jsx "^3.0.0"
+
+esprima@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
+
+esquery@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa"
+ dependencies:
+ estraverse "^4.0.0"
+
+esrecurse@^4.1.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163"
+ dependencies:
+ estraverse "^4.1.0"
+ object-assign "^4.0.1"
+
+estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+
+esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+
+event-emitter@~0.3.5:
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+
+events@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
+
+evp_bytestokey@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.0.tgz#497b66ad9fef65cd7c08a6180824ba1476b66e53"
+ dependencies:
+ create-hash "^1.1.1"
+
+execa@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
+ dependencies:
+ cross-spawn "^5.0.1"
+ get-stream "^3.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
+expand-brackets@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
+ dependencies:
+ is-posix-bracket "^0.1.0"
+
+expand-range@^1.8.1:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
+ dependencies:
+ fill-range "^2.1.0"
+
+extend@~3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
+
+external-editor@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.0.4.tgz#1ed9199da9cbfe2ef2f7a31b2fde8b0d12368972"
+ dependencies:
+ iconv-lite "^0.4.17"
+ jschardet "^1.4.2"
+ tmp "^0.0.31"
+
+extglob@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
+ dependencies:
+ is-extglob "^1.0.0"
+
+extsprintf@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550"
+
+fast-deep-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
+
+fast-levenshtein@~2.0.4:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+
+figures@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
+ dependencies:
+ escape-string-regexp "^1.0.5"
+
+file-entry-cache@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361"
+ dependencies:
+ flat-cache "^1.2.1"
+ object-assign "^4.0.1"
+
+filename-regex@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
+
+fill-range@^2.1.0:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723"
+ dependencies:
+ is-number "^2.1.0"
+ isobject "^2.0.0"
+ randomatic "^1.1.3"
+ repeat-element "^1.1.2"
+ repeat-string "^1.5.2"
+
+find-cache-dir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f"
+ dependencies:
+ commondir "^1.0.1"
+ make-dir "^1.0.0"
+ pkg-dir "^2.0.0"
+
+find-up@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
+ dependencies:
+ path-exists "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+find-up@^2.0.0, find-up@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+ dependencies:
+ locate-path "^2.0.0"
+
+flat-cache@^1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96"
+ dependencies:
+ circular-json "^0.3.1"
+ del "^2.0.2"
+ graceful-fs "^4.1.2"
+ write "^0.2.1"
+
+for-in@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+
+for-own@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
+ dependencies:
+ for-in "^1.0.1"
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+
+form-data@~2.1.1:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.5"
+ mime-types "^2.1.12"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+
+fsevents@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4"
+ dependencies:
+ nan "^2.3.0"
+ node-pre-gyp "^0.6.36"
+
+fstream-ignore@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
+ dependencies:
+ fstream "^1.0.0"
+ inherits "2"
+ minimatch "^3.0.0"
+
+fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
+ dependencies:
+ graceful-fs "^4.1.2"
+ inherits "~2.0.0"
+ mkdirp ">=0.5 0"
+ rimraf "2"
+
+function-bind@^1.0.2:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771"
+
+functional-red-black-tree@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
+
+gauge@~2.7.3:
+ version "2.7.4"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+ dependencies:
+ aproba "^1.0.3"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.0"
+ object-assign "^4.1.0"
+ signal-exit "^3.0.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wide-align "^1.1.0"
+
+get-caller-file@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
+
+get-stream@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
+
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ dependencies:
+ assert-plus "^1.0.0"
+
+glob-base@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
+ dependencies:
+ glob-parent "^2.0.0"
+ is-glob "^2.0.0"
+
+glob-parent@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28"
+ dependencies:
+ is-glob "^2.0.0"
+
+glob@^7.0.3, glob@^7.0.5, glob@^7.1.2:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+globals@^9.0.0, globals@^9.17.0:
+ version "9.18.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
+
+globby@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d"
+ dependencies:
+ array-union "^1.0.1"
+ arrify "^1.0.0"
+ glob "^7.0.3"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+graceful-fs@^4.1.2:
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
+
+har-schema@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
+
+har-validator@~4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
+ dependencies:
+ ajv "^4.9.1"
+ har-schema "^1.0.5"
+
+has-ansi@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+has-flag@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
+
+has-unicode@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+
+has@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
+ dependencies:
+ function-bind "^1.0.2"
+
+hash-base@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1"
+ dependencies:
+ inherits "^2.0.1"
+
+hash.js@^1.0.0, hash.js@^1.0.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846"
+ dependencies:
+ inherits "^2.0.3"
+ minimalistic-assert "^1.0.0"
+
+hawk@~3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
+ dependencies:
+ boom "2.x.x"
+ cryptiles "2.x.x"
+ hoek "2.x.x"
+ sntp "1.x.x"
+
+hmac-drbg@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
+ dependencies:
+ hash.js "^1.0.3"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.1"
+
+hoek@2.x.x:
+ version "2.16.3"
+ resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
+
+home-or-tmp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.1"
+
+hosted-git-info@^2.1.4:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c"
+
+http-signature@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
+ dependencies:
+ assert-plus "^0.2.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+https-browserify@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
+
+iconv-lite@^0.4.17:
+ version "0.4.18"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2"
+
+ieee754@^1.1.4:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
+
+ignore@^3.3.3:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.3.tgz#432352e57accd87ab3110e82d3fea0e47812156d"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+
+indexof@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+
+inherits@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+
+ini@~1.3.0:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
+
+inquirer@^3.0.6:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.2.1.tgz#06ceb0f540f45ca548c17d6840959878265fa175"
+ dependencies:
+ ansi-escapes "^2.0.0"
+ chalk "^2.0.0"
+ cli-cursor "^2.1.0"
+ cli-width "^2.0.0"
+ external-editor "^2.0.4"
+ figures "^2.0.0"
+ lodash "^4.3.0"
+ mute-stream "0.0.7"
+ run-async "^2.2.0"
+ rx-lite "^4.0.8"
+ rx-lite-aggregates "^4.0.8"
+ string-width "^2.1.0"
+ strip-ansi "^4.0.0"
+ through "^2.3.6"
+
+interpret@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90"
+
+invariant@^2.2.0, invariant@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
+ dependencies:
+ loose-envify "^1.0.0"
+
+invert-kv@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+
+is-binary-path@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
+ dependencies:
+ binary-extensions "^1.0.0"
+
+is-buffer@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
+
+is-builtin-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
+ dependencies:
+ builtin-modules "^1.0.0"
+
+is-dotfile@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
+
+is-equal-shallow@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534"
+ dependencies:
+ is-primitive "^2.0.0"
+
+is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+
+is-extglob@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0"
+
+is-finite@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+
+is-glob@^2.0.0, is-glob@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
+ dependencies:
+ is-extglob "^1.0.0"
+
+is-number@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-number@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-path-cwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
+
+is-path-in-cwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc"
+ dependencies:
+ is-path-inside "^1.0.0"
+
+is-path-inside@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f"
+ dependencies:
+ path-is-inside "^1.0.1"
+
+is-posix-bracket@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
+
+is-primitive@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
+
+is-promise@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
+
+is-resolvable@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62"
+ dependencies:
+ tryit "^1.0.1"
+
+is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+
+isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ dependencies:
+ isarray "1.0.0"
+
+isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+
+js-tokens@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+
+js-yaml@^3.8.4:
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.0.tgz#4ffbbf25c2ac963b8299dc74da7e3740de1c18ce"
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+
+jschardet@^1.4.2:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.5.0.tgz#a61f310306a5a71188e1b1acd08add3cfbb08b1e"
+
+jsesc@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
+
+jsesc@~0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
+
+json-loader@^0.5.4:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
+
+json-schema-traverse@^0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+
+json-stable-stringify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
+ dependencies:
+ jsonify "~0.0.0"
+
+json-stringify-safe@~5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+
+json5@^0.5.0, json5@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+
+jsonify@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+
+jsprim@^1.2.2:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918"
+ dependencies:
+ assert-plus "1.0.0"
+ extsprintf "1.0.2"
+ json-schema "0.2.3"
+ verror "1.3.6"
+
+kind-of@^3.0.2:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+ dependencies:
+ is-buffer "^1.1.5"
+
+lazy-cache@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
+
+lcid@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
+ dependencies:
+ invert-kv "^1.0.0"
+
+levn@^0.3.0, levn@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+ dependencies:
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+
+load-json-file@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ strip-bom "^3.0.0"
+
+loader-runner@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
+
+loader-utils@^1.0.2, loader-utils@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
+ dependencies:
+ big.js "^3.1.3"
+ emojis-list "^2.0.0"
+ json5 "^0.5.0"
+
+locate-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+ dependencies:
+ p-locate "^2.0.0"
+ path-exists "^3.0.0"
+
+lodash.cond@^4.3.0:
+ version "4.5.2"
+ resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"
+
+lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0:
+ version "4.17.4"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
+
+longest@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
+
+loose-envify@^1.0.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
+ dependencies:
+ js-tokens "^3.0.0"
+
+lru-cache@^4.0.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
+ dependencies:
+ pseudomap "^1.0.2"
+ yallist "^2.1.2"
+
+make-dir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978"
+ dependencies:
+ pify "^2.3.0"
+
+mem@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
+ dependencies:
+ mimic-fn "^1.0.0"
+
+memory-fs@^0.4.0, memory-fs@~0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
+ dependencies:
+ errno "^0.1.3"
+ readable-stream "^2.0.1"
+
+micromatch@^2.1.5:
+ version "2.3.11"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
+ dependencies:
+ arr-diff "^2.0.0"
+ array-unique "^0.2.1"
+ braces "^1.8.2"
+ expand-brackets "^0.1.4"
+ extglob "^0.3.1"
+ filename-regex "^2.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.1"
+ kind-of "^3.0.2"
+ normalize-path "^2.0.1"
+ object.omit "^2.0.0"
+ parse-glob "^3.0.4"
+ regex-cache "^0.4.2"
+
+miller-rabin@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.0.tgz#4a62fb1d42933c05583982f4c716f6fb9e6c6d3d"
+ dependencies:
+ bn.js "^4.0.0"
+ brorand "^1.0.1"
+
+mime-db@~1.29.0:
+ version "1.29.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878"
+
+mime-types@^2.1.12, mime-types@~2.1.7:
+ version "2.1.16"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23"
+ dependencies:
+ mime-db "~1.29.0"
+
+mimic-fn@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
+
+minimalistic-assert@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
+
+minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
+
+minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+
+minimist@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+
+"mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@~0.5.0:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ dependencies:
+ minimist "0.0.8"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+
+mute-stream@0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
+
+nan@^2.3.0:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+
+node-libs-browser@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646"
+ dependencies:
+ assert "^1.1.1"
+ browserify-zlib "^0.1.4"
+ buffer "^4.3.0"
+ console-browserify "^1.1.0"
+ constants-browserify "^1.0.0"
+ crypto-browserify "^3.11.0"
+ domain-browser "^1.1.1"
+ events "^1.0.0"
+ https-browserify "0.0.1"
+ os-browserify "^0.2.0"
+ path-browserify "0.0.0"
+ process "^0.11.0"
+ punycode "^1.2.4"
+ querystring-es3 "^0.2.0"
+ readable-stream "^2.0.5"
+ stream-browserify "^2.0.1"
+ stream-http "^2.3.1"
+ string_decoder "^0.10.25"
+ timers-browserify "^2.0.2"
+ tty-browserify "0.0.0"
+ url "^0.11.0"
+ util "^0.10.3"
+ vm-browserify "0.0.4"
+
+node-pre-gyp@^0.6.36:
+ version "0.6.36"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786"
+ dependencies:
+ mkdirp "^0.5.1"
+ nopt "^4.0.1"
+ npmlog "^4.0.2"
+ rc "^1.1.7"
+ request "^2.81.0"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^2.2.1"
+ tar-pack "^3.4.0"
+
+nopt@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
+ dependencies:
+ abbrev "1"
+ osenv "^0.1.4"
+
+normalize-package-data@^2.3.2:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
+ dependencies:
+ hosted-git-info "^2.1.4"
+ is-builtin-module "^1.0.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+normalize-path@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
+npm-run-path@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+ dependencies:
+ path-key "^2.0.0"
+
+npmlog@^4.0.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+ dependencies:
+ are-we-there-yet "~1.1.2"
+ console-control-strings "~1.1.0"
+ gauge "~2.7.3"
+ set-blocking "~2.0.0"
+
+number-is-nan@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+
+oauth-sign@~0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
+
+object-assign@^4.0.1, object-assign@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+
+object.omit@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
+ dependencies:
+ for-own "^0.1.4"
+ is-extendable "^0.1.1"
+
+once@^1.3.0, once@^1.3.3:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ dependencies:
+ wrappy "1"
+
+onetime@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
+ dependencies:
+ mimic-fn "^1.0.0"
+
+optionator@^0.8.2:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
+ dependencies:
+ deep-is "~0.1.3"
+ fast-levenshtein "~2.0.4"
+ levn "~0.3.0"
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+ wordwrap "~1.0.0"
+
+os-browserify@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f"
+
+os-homedir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+
+os-locale@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2"
+ dependencies:
+ execa "^0.7.0"
+ lcid "^1.0.0"
+ mem "^1.1.0"
+
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+
+osenv@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.0"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+
+p-limit@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc"
+
+p-locate@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+ dependencies:
+ p-limit "^1.1.0"
+
+pako@~0.2.0:
+ version "0.2.9"
+ resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
+
+parse-asn1@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712"
+ dependencies:
+ asn1.js "^4.0.0"
+ browserify-aes "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.0"
+ pbkdf2 "^3.0.3"
+
+parse-glob@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
+ dependencies:
+ glob-base "^0.3.0"
+ is-dotfile "^1.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.0"
+
+parse-json@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+ dependencies:
+ error-ex "^1.2.0"
+
+path-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
+
+path-exists@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
+ dependencies:
+ pinkie-promise "^2.0.0"
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+
+path-is-inside@^1.0.1, path-is-inside@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+
+path-key@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+
+path-parse@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
+
+path-type@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
+ dependencies:
+ pify "^2.0.0"
+
+pbkdf2@^3.0.3:
+ version "3.0.12"
+ resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.12.tgz#be36785c5067ea48d806ff923288c5f750b6b8a2"
+ dependencies:
+ create-hash "^1.1.2"
+ create-hmac "^1.1.4"
+ ripemd160 "^2.0.1"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+performance-now@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
+
+pify@^2.0.0, pify@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+
+pinkie-promise@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
+ dependencies:
+ pinkie "^2.0.0"
+
+pinkie@^2.0.0:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
+
+pkg-dir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4"
+ dependencies:
+ find-up "^1.0.0"
+
+pkg-dir@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
+ dependencies:
+ find-up "^2.1.0"
+
+pluralize@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-4.0.0.tgz#59b708c1c0190a2f692f1c7618c446b052fd1762"
+
+prelude-ls@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+
+preserve@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
+
+private@^0.1.6:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
+
+process-nextick-args@~1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
+
+process@^0.11.0:
+ version "0.11.10"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+
+progress@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
+
+prr@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
+
+pseudomap@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+
+public-encrypt@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6"
+ dependencies:
+ bn.js "^4.1.0"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ parse-asn1 "^5.0.0"
+ randombytes "^2.0.1"
+
+punycode@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+
+punycode@^1.2.4, punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+
+qs@~6.4.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
+
+querystring-es3@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
+
+querystring@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+
+randomatic@^1.1.3:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c"
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
+randombytes@^2.0.0, randombytes@^2.0.1:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79"
+ dependencies:
+ safe-buffer "^5.1.0"
+
+rc@^1.1.7:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
+ dependencies:
+ deep-extend "~0.4.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+read-pkg-up@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
+ dependencies:
+ find-up "^2.0.0"
+ read-pkg "^2.0.0"
+
+read-pkg@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
+ dependencies:
+ load-json-file "^2.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^2.0.0"
+
+readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.6:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~1.0.6"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.0.3"
+ util-deprecate "~1.0.1"
+
+readdirp@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
+ dependencies:
+ graceful-fs "^4.1.2"
+ minimatch "^3.0.2"
+ readable-stream "^2.0.2"
+ set-immediate-shim "^1.0.1"
+
+regenerate@^1.2.1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260"
+
+regenerator-runtime@^0.10.0:
+ version "0.10.5"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658"
+
+regenerator-transform@0.9.11:
+ version "0.9.11"
+ resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.9.11.tgz#3a7d067520cb7b7176769eb5ff868691befe1283"
+ dependencies:
+ babel-runtime "^6.18.0"
+ babel-types "^6.19.0"
+ private "^0.1.6"
+
+regex-cache@^0.4.2:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145"
+ dependencies:
+ is-equal-shallow "^0.1.3"
+ is-primitive "^2.0.0"
+
+regexpu-core@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240"
+ dependencies:
+ regenerate "^1.2.1"
+ regjsgen "^0.2.0"
+ regjsparser "^0.1.4"
+
+regjsgen@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7"
+
+regjsparser@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c"
+ dependencies:
+ jsesc "~0.5.0"
+
+remove-trailing-separator@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz#69b062d978727ad14dc6b56ba4ab772fd8d70511"
+
+repeat-element@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a"
+
+repeat-string@^1.5.2:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+
+repeating@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+ dependencies:
+ is-finite "^1.0.0"
+
+request@^2.81.0:
+ version "2.81.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
+ dependencies:
+ aws-sign2 "~0.6.0"
+ aws4 "^1.2.1"
+ caseless "~0.12.0"
+ combined-stream "~1.0.5"
+ extend "~3.0.0"
+ forever-agent "~0.6.1"
+ form-data "~2.1.1"
+ har-validator "~4.2.1"
+ hawk "~3.1.3"
+ http-signature "~1.1.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.7"
+ oauth-sign "~0.8.1"
+ performance-now "^0.2.0"
+ qs "~6.4.0"
+ safe-buffer "^5.0.1"
+ stringstream "~0.0.4"
+ tough-cookie "~2.3.0"
+ tunnel-agent "^0.6.0"
+ uuid "^3.0.0"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+
+require-main-filename@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+
+require-uncached@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
+ dependencies:
+ caller-path "^0.1.0"
+ resolve-from "^1.0.0"
+
+resolve-from@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
+
+resolve@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86"
+ dependencies:
+ path-parse "^1.0.5"
+
+restore-cursor@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
+ dependencies:
+ onetime "^2.0.0"
+ signal-exit "^3.0.2"
+
+right-align@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
+ dependencies:
+ align-text "^0.1.1"
+
+rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d"
+ dependencies:
+ glob "^7.0.5"
+
+ripemd160@^2.0.0, ripemd160@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7"
+ dependencies:
+ hash-base "^2.0.0"
+ inherits "^2.0.1"
+
+run-async@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
+ dependencies:
+ is-promise "^2.1.0"
+
+rx-lite-aggregates@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be"
+ dependencies:
+ rx-lite "*"
+
+rx-lite@*, rx-lite@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+
+"semver@2 || 3 || 4 || 5", semver@^5.3.0:
+ version "5.4.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
+
+set-blocking@^2.0.0, set-blocking@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+
+set-immediate-shim@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
+
+setimmediate@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+
+sha.js@^2.4.0, sha.js@^2.4.8:
+ version "2.4.8"
+ resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.8.tgz#37068c2c476b6baf402d14a49c67f597921f634f"
+ dependencies:
+ inherits "^2.0.1"
+
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ dependencies:
+ shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+
+signal-exit@^3.0.0, signal-exit@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+
+slash@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
+
+slice-ansi@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
+
+sntp@1.x.x:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
+ dependencies:
+ hoek "2.x.x"
+
+source-list-map@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
+
+source-map-support@^0.4.2:
+ version "0.4.15"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.15.tgz#03202df65c06d2bd8c7ec2362a193056fef8d3b1"
+ dependencies:
+ source-map "^0.5.6"
+
+source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3:
+ version "0.5.6"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
+
+spark-md5@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.0.tgz#3722227c54e2faf24b1dc6d933cc144e6f71bfef"
+
+spdx-correct@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
+ dependencies:
+ spdx-license-ids "^1.0.2"
+
+spdx-expression-parse@~1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c"
+
+spdx-license-ids@^1.0.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57"
+
+sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+
+sshpk@^1.7.0:
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3"
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ dashdash "^1.12.0"
+ getpass "^0.1.1"
+ optionalDependencies:
+ bcrypt-pbkdf "^1.0.0"
+ ecc-jsbn "~0.1.1"
+ jsbn "~0.1.0"
+ tweetnacl "~0.14.0"
+
+stream-browserify@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
+ dependencies:
+ inherits "~2.0.1"
+ readable-stream "^2.0.2"
+
+stream-http@^2.3.1:
+ version "2.7.2"
+ resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad"
+ dependencies:
+ builtin-status-codes "^3.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.2.6"
+ to-arraybuffer "^1.0.0"
+ xtend "^4.0.0"
+
+string-width@^1.0.1, string-width@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
+string-width@^2.0.0, string-width@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
+string_decoder@^0.10.25:
+ version "0.10.31"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+
+string_decoder@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
+ dependencies:
+ safe-buffer "~5.1.0"
+
+stringstream@~0.0.4:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+ dependencies:
+ ansi-regex "^3.0.0"
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+
+strip-json-comments@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+
+supports-color@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+
+supports-color@^4.0.0, supports-color@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.1.tgz#65a4bb2631e90e02420dba5554c375a4754bb836"
+ dependencies:
+ has-flag "^2.0.0"
+
+table@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/table/-/table-4.0.1.tgz#a8116c133fac2c61f4a420ab6cdf5c4d61f0e435"
+ dependencies:
+ ajv "^4.7.0"
+ ajv-keywords "^1.0.0"
+ chalk "^1.1.1"
+ lodash "^4.0.0"
+ slice-ansi "0.0.4"
+ string-width "^2.0.0"
+
+tapable@^0.2.7:
+ version "0.2.7"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.7.tgz#e46c0daacbb2b8a98b9b0cea0f4052105817ed5c"
+
+tar-pack@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984"
+ dependencies:
+ debug "^2.2.0"
+ fstream "^1.0.10"
+ fstream-ignore "^1.0.5"
+ once "^1.3.3"
+ readable-stream "^2.1.4"
+ rimraf "^2.5.1"
+ tar "^2.2.1"
+ uid-number "^0.0.6"
+
+tar@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
+ dependencies:
+ block-stream "*"
+ fstream "^1.0.2"
+ inherits "2"
+
+text-table@~0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+
+through@^2.3.6:
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+
+timers-browserify@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.2.tgz#ab4883cf597dcd50af211349a00fbca56ac86b86"
+ dependencies:
+ setimmediate "^1.0.4"
+
+tmp@^0.0.31:
+ version "0.0.31"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7"
+ dependencies:
+ os-tmpdir "~1.0.1"
+
+to-arraybuffer@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
+
+to-fast-properties@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
+
+tough-cookie@~2.3.0:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a"
+ dependencies:
+ punycode "^1.4.1"
+
+trim-right@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+
+tryit@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb"
+
+tty-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ dependencies:
+ safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+
+type-check@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+ dependencies:
+ prelude-ls "~1.1.2"
+
+typedarray@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+
+uglify-js@^2.8.29:
+ version "2.8.29"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
+ dependencies:
+ source-map "~0.5.1"
+ yargs "~3.10.0"
+ optionalDependencies:
+ uglify-to-browserify "~1.0.0"
+
+uglify-to-browserify@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
+
+uglifyjs-webpack-plugin@^0.4.6:
+ version "0.4.6"
+ resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309"
+ dependencies:
+ source-map "^0.5.6"
+ uglify-js "^2.8.29"
+ webpack-sources "^1.0.1"
+
+uid-number@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
+
+url@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
+ dependencies:
+ punycode "1.3.2"
+ querystring "0.2.0"
+
+util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+
+util@0.10.3, util@^0.10.3:
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
+ dependencies:
+ inherits "2.0.1"
+
+uuid@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"
+ dependencies:
+ spdx-correct "~1.0.0"
+ spdx-expression-parse "~1.0.0"
+
+verror@1.3.6:
+ version "1.3.6"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c"
+ dependencies:
+ extsprintf "1.0.2"
+
+vm-browserify@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
+ dependencies:
+ indexof "0.0.1"
+
+watchpack@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac"
+ dependencies:
+ async "^2.1.2"
+ chokidar "^1.7.0"
+ graceful-fs "^4.1.2"
+
+webpack-sources@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf"
+ dependencies:
+ source-list-map "^2.0.0"
+ source-map "~0.5.3"
+
+webpack@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.4.0.tgz#e9465b660ad79dd2d33874d968b31746ea9a8e63"
+ dependencies:
+ acorn "^5.0.0"
+ acorn-dynamic-import "^2.0.0"
+ ajv "^5.1.5"
+ ajv-keywords "^2.0.0"
+ async "^2.1.2"
+ enhanced-resolve "^3.4.0"
+ escope "^3.6.0"
+ interpret "^1.0.0"
+ json-loader "^0.5.4"
+ json5 "^0.5.1"
+ loader-runner "^2.3.0"
+ loader-utils "^1.1.0"
+ memory-fs "~0.4.1"
+ mkdirp "~0.5.0"
+ node-libs-browser "^2.0.0"
+ source-map "^0.5.3"
+ supports-color "^4.2.1"
+ tapable "^0.2.7"
+ uglifyjs-webpack-plugin "^0.4.6"
+ watchpack "^1.4.0"
+ webpack-sources "^1.0.1"
+ yargs "^8.0.2"
+
+which-module@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+
+which@^1.2.9:
+ version "1.2.14"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5"
+ dependencies:
+ isexe "^2.0.0"
+
+wide-align@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
+ dependencies:
+ string-width "^1.0.2"
+
+window-size@0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
+
+wordwrap@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
+
+wordwrap@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+
+wrap-ansi@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+
+write@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"
+ dependencies:
+ mkdirp "^0.5.1"
+
+xtend@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
+
+y18n@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
+
+yallist@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+
+yargs-parser@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9"
+ dependencies:
+ camelcase "^4.1.0"
+
+yargs@^8.0.2:
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360"
+ dependencies:
+ camelcase "^4.1.0"
+ cliui "^3.2.0"
+ decamelize "^1.1.1"
+ get-caller-file "^1.0.1"
+ os-locale "^2.0.0"
+ read-pkg-up "^2.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^7.0.0"
+
+yargs@~3.10.0:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"
+ dependencies:
+ camelcase "^1.0.2"
+ cliui "^2.1.0"
+ decamelize "^1.0.0"
+ window-size "0.1.0"