aboutsummaryrefslogtreecommitdiffstats
path: root/activestorage
diff options
context:
space:
mode:
Diffstat (limited to 'activestorage')
-rw-r--r--activestorage/CHANGELOG.md23
-rw-r--r--activestorage/MIT-LICENSE2
-rw-r--r--activestorage/README.md4
-rw-r--r--activestorage/activestorage.gemspec4
-rw-r--r--activestorage/app/assets/javascripts/activestorage.js2
-rw-r--r--activestorage/app/controllers/active_storage/blobs_controller.rb10
-rw-r--r--activestorage/app/controllers/active_storage/previews_controller.rb10
-rw-r--r--activestorage/app/controllers/active_storage/variants_controller.rb10
-rw-r--r--activestorage/app/controllers/concerns/active_storage/set_blob.rb16
-rw-r--r--activestorage/app/javascript/activestorage/blob_record.js20
-rw-r--r--activestorage/app/javascript/activestorage/helpers.js11
-rw-r--r--activestorage/app/models/active_storage/attachment.rb6
-rw-r--r--activestorage/app/models/active_storage/blob.rb146
-rw-r--r--activestorage/app/models/active_storage/blob/analyzable.rb57
-rw-r--r--activestorage/app/models/active_storage/blob/identifiable.rb11
-rw-r--r--activestorage/app/models/active_storage/blob/representable.rb93
-rw-r--r--activestorage/app/models/active_storage/filename.rb2
-rw-r--r--activestorage/app/models/active_storage/filename/parameters.rb2
-rw-r--r--activestorage/app/models/active_storage/identification.rb38
-rw-r--r--activestorage/app/models/active_storage/variant.rb56
-rw-r--r--activestorage/app/models/active_storage/variation.rb24
-rw-r--r--activestorage/config/routes.rb14
-rw-r--r--activestorage/lib/active_storage.rb9
-rw-r--r--activestorage/lib/active_storage/analyzer.rb2
-rw-r--r--activestorage/lib/active_storage/analyzer/image_analyzer.rb3
-rw-r--r--activestorage/lib/active_storage/analyzer/video_analyzer.rb65
-rw-r--r--activestorage/lib/active_storage/attached/macros.rb8
-rw-r--r--activestorage/lib/active_storage/downloading.rb10
-rw-r--r--activestorage/lib/active_storage/engine.rb32
-rw-r--r--activestorage/lib/active_storage/errors.rb7
-rw-r--r--activestorage/lib/active_storage/previewer.rb14
-rw-r--r--activestorage/lib/active_storage/previewer/pdf_previewer.rb6
-rw-r--r--activestorage/lib/active_storage/previewer/video_previewer.rb6
-rw-r--r--activestorage/lib/active_storage/service.rb2
-rw-r--r--activestorage/lib/active_storage/service/azure_storage_service.rb2
-rw-r--r--activestorage/lib/active_storage/service/disk_service.rb28
-rw-r--r--activestorage/lib/tasks/activestorage.rake6
-rw-r--r--activestorage/test/analyzer/image_analyzer_test.rb10
-rw-r--r--activestorage/test/analyzer/video_analyzer_test.rb34
-rw-r--r--activestorage/test/controllers/blobs_controller_test.rb5
-rw-r--r--activestorage/test/controllers/previews_controller_test.rb9
-rw-r--r--activestorage/test/controllers/variants_controller_test.rb9
-rw-r--r--activestorage/test/database/setup.rb2
-rw-r--r--activestorage/test/dummy/config/application.rb4
-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/video_with_rectangular_samples.mp4bin0 -> 361535 bytes
-rw-r--r--activestorage/test/fixtures/files/video_with_undefined_display_aspect_ratio.mp4bin0 -> 128737 bytes
-rw-r--r--activestorage/test/models/attachments_test.rb25
-rw-r--r--activestorage/test/models/blob_test.rb36
-rw-r--r--activestorage/test/models/preview_test.rb2
-rw-r--r--activestorage/test/models/representation_test.rb2
-rw-r--r--activestorage/test/models/variant_test.rb58
-rw-r--r--activestorage/test/service/configurator_test.rb3
-rw-r--r--activestorage/test/service/mirror_service_test.rb4
-rw-r--r--activestorage/test/test_helper.rb3
-rw-r--r--activestorage/webpack.config.js1
58 files changed, 695 insertions, 286 deletions
diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md
index c5171e7490..061898d143 100644
--- a/activestorage/CHANGELOG.md
+++ b/activestorage/CHANGELOG.md
@@ -1,3 +1,26 @@
+* Preserve display aspect ratio when extracting width and height from videos
+ with rectangular samples in `ActiveStorage::Analyzer::VideoAnalyzer`.
+
+ When a video contains a display aspect ratio, emit it in metadata as
+ `:display_aspect_ratio` rather than the ambiguous `:aspect_ratio`. Compute
+ its height by scaling its encoded frame width according to the DAR.
+
+ *George Claghorn*
+
+* Use `after_destroy_commit` instead of `before_destroy` for purging
+ attachments when a record is destroyed.
+
+ *Hiroki Zenigami*
+
+
+* Force `:attachment` disposition for specific, configurable content types.
+ This mitigates possible security issues such as XSS or phishing when
+ serving them inline. A list of such content types is included by default,
+ and can be configured via `content_types_to_serve_as_binary`.
+
+ *Rosa Gutierrez*
+
+
## Rails 5.2.0.beta2 (November 28, 2017) ##
* Fix the gem adding the migrations files to the package.
diff --git a/activestorage/MIT-LICENSE b/activestorage/MIT-LICENSE
index 4e1c6cad79..eed89ac398 100644
--- a/activestorage/MIT-LICENSE
+++ b/activestorage/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2017 David Heinemeier Hansson, Basecamp
+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
diff --git a/activestorage/README.md b/activestorage/README.md
index 8af0409ec5..85ab70dac6 100644
--- a/activestorage/README.md
+++ b/activestorage/README.md
@@ -1,6 +1,6 @@
# Active Storage
-Active Storage makes it simple to upload and reference files in cloud services like Amazon S3, Google Cloud Storage, or Microsoft Azure 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.
+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.
@@ -144,7 +144,7 @@ Active Storage, with its included JavaScript library, supports uploading directl
Active Storage is released under the [MIT License](https://opensource.org/licenses/MIT).
- ## Support
+## Support
API documentation is at:
diff --git a/activestorage/activestorage.gemspec b/activestorage/activestorage.gemspec
index 7f7f1a26ac..d135324700 100644
--- a/activestorage/activestorage.gemspec
+++ b/activestorage/activestorage.gemspec
@@ -27,4 +27,8 @@ Gem::Specification.new do |s|
s.add_dependency "actionpack", version
s.add_dependency "activerecord", version
+
+ s.add_dependency "marcel", "~> 0.3.1"
+
+ s.add_development_dependency "webmock", "~> 3.2.1"
end
diff --git a/activestorage/app/assets/javascripts/activestorage.js b/activestorage/app/assets/javascripts/activestorage.js
index c1c0a2f6d9..a2f06c43a3 100644
--- a/activestorage/app/assets/javascripts/activestorage.js
+++ b/activestorage/app/assets/javascripts/activestorage.js
@@ -1 +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){var e=this.xhr,r=e.status,n=e.response;if(r>=200&&r<300){var i=n.direct_upload;delete n.direct_upload,this.attributes=n,this.directUploadData=i,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.xhr.status)}},{key:"toJSON",value:function(){var t={};for(var e in this.attributes)t[e]=this.attributes[e];return t}}]),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
+!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=t.disabled,i=r.bubbles,a=r.cancelable,u=r.detail,o=document.createEvent("Event");o.initEvent(e,i||!0,a||!0),o.detail=u||{};try{t.disabled=!1,t.dispatchEvent(o)}finally{t.disabled=n}return o}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
index a17e3852f9..fa44131048 100644
--- a/activestorage/app/controllers/active_storage/blobs_controller.rb
+++ b/activestorage/app/controllers/active_storage/blobs_controller.rb
@@ -5,12 +5,10 @@
# 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
- if blob = ActiveStorage::Blob.find_signed(params[:signed_id])
- expires_in ActiveStorage::Blob.service.url_expires_in
- redirect_to blob.service_url(disposition: params[:disposition])
- else
- head :not_found
- end
+ 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/previews_controller.rb b/activestorage/app/controllers/active_storage/previews_controller.rb
index 9e8cf27b6e..aa7ef58ca4 100644
--- a/activestorage/app/controllers/active_storage/previews_controller.rb
+++ b/activestorage/app/controllers/active_storage/previews_controller.rb
@@ -1,12 +1,10 @@
# frozen_string_literal: true
class ActiveStorage::PreviewsController < ActionController::Base
+ include ActiveStorage::SetBlob
+
def show
- if blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id])
- expires_in ActiveStorage::Blob.service.url_expires_in
- redirect_to ActiveStorage::Preview.new(blob, params[:variation_key]).processed.service_url(disposition: params[:disposition])
- else
- head :not_found
- end
+ 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
index dc5e78ecc0..e8f8dd592d 100644
--- a/activestorage/app/controllers/active_storage/variants_controller.rb
+++ b/activestorage/app/controllers/active_storage/variants_controller.rb
@@ -5,12 +5,10 @@
# 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
- if blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id])
- expires_in ActiveStorage::Blob.service.url_expires_in
- redirect_to ActiveStorage::Variant.new(blob, params[:variation_key]).processed.service_url(disposition: params[:disposition])
- else
- head :not_found
- end
+ 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..f072954d78
--- /dev/null
+++ b/activestorage/app/controllers/concerns/active_storage/set_blob.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module ActiveStorage::SetBlob #:nodoc:
+ 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
index 3c6e6b6ba1..ff847892b2 100644
--- a/activestorage/app/javascript/activestorage/blob_record.js
+++ b/activestorage/app/javascript/activestorage/blob_record.js
@@ -22,14 +22,28 @@ export class BlobRecord {
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) {
- const { status, response } = this.xhr
- if (status >= 200 && status < 300) {
+ if (this.status >= 200 && this.status < 300) {
+ const { response } = this
const { direct_upload } = response
delete response.direct_upload
this.attributes = response
@@ -41,7 +55,7 @@ export class BlobRecord {
}
requestDidError(event) {
- this.callback(`Error creating Blob for "${this.file.name}". Status: ${this.xhr.status}`)
+ this.callback(`Error creating Blob for "${this.file.name}". Status: ${this.status}`)
}
toJSON() {
diff --git a/activestorage/app/javascript/activestorage/helpers.js b/activestorage/app/javascript/activestorage/helpers.js
index 52fec8f6f1..7e83c447e7 100644
--- a/activestorage/app/javascript/activestorage/helpers.js
+++ b/activestorage/app/javascript/activestorage/helpers.js
@@ -23,11 +23,20 @@ export function findElement(root, selector) {
}
export function dispatchEvent(element, type, eventInit = {}) {
+ const { disabled } = element
const { bubbles, cancelable, detail } = eventInit
const event = document.createEvent("Event")
+
event.initEvent(type, bubbles || true, cancelable || true)
event.detail = detail || {}
- element.dispatchEvent(event)
+
+ try {
+ element.disabled = false
+ element.dispatchEvent(event)
+ } finally {
+ element.disabled = disabled
+ }
+
return event
}
diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb
index 9f61a5dbf3..19f48c57d6 100644
--- a/activestorage/app/models/active_storage/attachment.rb
+++ b/activestorage/app/models/active_storage/attachment.rb
@@ -14,7 +14,7 @@ class ActiveStorage::Attachment < ActiveRecord::Base
delegate_missing_to :blob
- after_create_commit :analyze_blob_later
+ after_create_commit :identify_blob, :analyze_blob_later
# Synchronously purges the blob (deletes it from the configured service) and destroys the attachment.
def purge
@@ -29,6 +29,10 @@ class ActiveStorage::Attachment < ActiveRecord::Base
end
private
+ def identify_blob
+ blob.identify
+ end
+
def analyze_blob_later
blob.analyze_later unless blob.analyzed?
end
diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb
index acaf22fac1..7067c58259 100644
--- a/activestorage/app/models/active_storage/blob.rb
+++ b/activestorage/app/models/active_storage/blob.rb
@@ -1,7 +1,5 @@
# 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:
#
@@ -16,20 +14,17 @@ require "active_storage/analyzer/null_analyzer"
# 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 UnpreviewableError < StandardError; end
- class UnrepresentableError < StandardError; end
+ include Analyzable, Identifiable, Representable
self.table_name = "active_storage_blobs"
has_secure_token :key
- store :metadata, accessors: [ :analyzed ], coder: JSON
+ store :metadata, accessors: [ :analyzed, :identified ], 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
@@ -68,7 +63,6 @@ class ActiveStorage::Blob < ActiveRecord::Base
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
@@ -110,85 +104,13 @@ class ActiveStorage::Blob < ActiveRecord::Base
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.
- def variant(transformations)
- ActiveStorage::Variant.new(self, ActiveStorage::Variation.wrap(transformations))
- 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 instance for a previewable blob or an ActiveStorage::Variant instance for an image blob.
- #
- # blob.representation(resize: "100x100").processed.service_url
- #
- # Raises ActiveStorage::Blob::UnrepresentableError if the receiving blob is neither an image 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 image?
- variant transformations
- else
- raise UnrepresentableError
- end
- end
-
- # Returns true if the blob is an image or is previewable.
- def representable?
- image? || 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
+ def service_url(expires_in: service.url_expires_in, disposition: :inline, filename: self.filename)
+ service.url key, expires_in: expires_in, disposition: forcibly_serve_as_binary? ? :attachment : 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
@@ -202,6 +124,7 @@ class ActiveStorage::Blob < ActiveRecord::Base
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.
@@ -213,8 +136,10 @@ class ActiveStorage::Blob < ActiveRecord::Base
# 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
+ self.checksum = compute_checksum_in_chunks(io)
+ self.content_type = extract_content_type(io)
+ self.byte_size = io.size
+ self.identified = true
service.upload(key, io, checksum: checksum)
end
@@ -226,46 +151,6 @@ class ActiveStorage::Blob < ActiveRecord::Base
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.
@@ -299,16 +184,11 @@ class ActiveStorage::Blob < ActiveRecord::Base
end.base64digest
end
-
- def extract_metadata_via_analyzer
- analyzer.metadata.merge(analyzed: true)
- end
-
- def analyzer
- analyzer_class.new(self)
+ def extract_content_type(io)
+ Marcel::MimeType.for io, name: filename.to_s, declared_type: content_type
end
- def analyzer_class
- ActiveStorage.analyzers.detect { |klass| klass.accept?(self) } || ActiveStorage::Analyzer::NullAnalyzer
+ def forcibly_serve_as_binary?
+ ActiveStorage.content_types_to_serve_as_binary.include?(content_type)
end
end
diff --git a/activestorage/app/models/active_storage/blob/analyzable.rb b/activestorage/app/models/active_storage/blob/analyzable.rb
new file mode 100644
index 0000000000..5bda6e6d73
--- /dev/null
+++ b/activestorage/app/models/active_storage/blob/analyzable.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require "active_storage/analyzer/null_analyzer"
+
+module ActiveStorage::Blob::Analyzable
+ # 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
+
+ private
+ 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/blob/identifiable.rb b/activestorage/app/models/active_storage/blob/identifiable.rb
new file mode 100644
index 0000000000..40ca84ac70
--- /dev/null
+++ b/activestorage/app/models/active_storage/blob/identifiable.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module ActiveStorage::Blob::Identifiable
+ def identify
+ ActiveStorage::Identification.new(self).apply
+ end
+
+ def identified?
+ identified
+ end
+end
diff --git a/activestorage/app/models/active_storage/blob/representable.rb b/activestorage/app/models/active_storage/blob/representable.rb
new file mode 100644
index 0000000000..0ad2e2fd77
--- /dev/null
+++ b/activestorage/app/models/active_storage/blob/representable.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+module ActiveStorage::Blob::Representable
+ extend ActiveSupport::Concern
+
+ included do
+ has_one_attached :preview_image
+ 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::InvariableError if ImageMagick cannot transform the blob. To determine whether a blob is
+ # variable, call ActiveStorage::Blob#variable?.
+ def variant(transformations)
+ if variable?
+ ActiveStorage::Variant.new(self, ActiveStorage::Variation.wrap(transformations))
+ else
+ raise ActiveStorage::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::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 ActiveStorage::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::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 ActiveStorage::UnrepresentableError
+ end
+ end
+
+ # Returns true if the blob is variable or previewable.
+ def representable?
+ variable? || previewable?
+ end
+end
diff --git a/activestorage/app/models/active_storage/filename.rb b/activestorage/app/models/active_storage/filename.rb
index 79d55dc889..b9413dec95 100644
--- a/activestorage/app/models/active_storage/filename.rb
+++ b/activestorage/app/models/active_storage/filename.rb
@@ -50,7 +50,7 @@ class ActiveStorage::Filename
@filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
end
- def parameters
+ def parameters #:nodoc:
Parameters.new self
end
diff --git a/activestorage/app/models/active_storage/filename/parameters.rb b/activestorage/app/models/active_storage/filename/parameters.rb
index 58ce198d38..fb9ea10e49 100644
--- a/activestorage/app/models/active_storage/filename/parameters.rb
+++ b/activestorage/app/models/active_storage/filename/parameters.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class ActiveStorage::Filename::Parameters
+class ActiveStorage::Filename::Parameters #:nodoc:
attr_reader :filename
def initialize(filename)
diff --git a/activestorage/app/models/active_storage/identification.rb b/activestorage/app/models/active_storage/identification.rb
new file mode 100644
index 0000000000..4f295257ae
--- /dev/null
+++ b/activestorage/app/models/active_storage/identification.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class ActiveStorage::Identification
+ attr_reader :blob
+
+ def initialize(blob)
+ @blob = blob
+ end
+
+ def apply
+ blob.update!(content_type: content_type, identified: true) unless blob.identified?
+ end
+
+ private
+ def content_type
+ Marcel::MimeType.for(identifiable_chunk, name: filename, declared_type: declared_content_type)
+ end
+
+
+ def identifiable_chunk
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |client|
+ client.get(uri, "Range" => "0-4096").body
+ end
+ end
+
+ def uri
+ @uri ||= URI.parse(blob.service_url)
+ end
+
+
+ def filename
+ blob.filename.to_s
+ end
+
+ def declared_content_type
+ blob.content_type
+ end
+end
diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb
index fa5aa69bd3..e08a2271ec 100644
--- a/activestorage/app/models/active_storage/variant.rb
+++ b/activestorage/app/models/active_storage/variant.rb
@@ -1,5 +1,7 @@
# 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.
@@ -16,7 +18,7 @@
# 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 url_for(Current.user.avatar.variant(resize: "100x100")) %>
+# <%= 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.
@@ -35,6 +37,10 @@
#
# 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
@@ -62,7 +68,7 @@ class ActiveStorage::Variant
# 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: blob.filename, content_type: blob.content_type
+ 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.
@@ -76,11 +82,51 @@ class ActiveStorage::Variant
end
def process
- service.upload key, transform(service.download(blob.key))
+ 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 transform(io)
+ def download_image
require "mini_magick"
- File.open MiniMagick::Image.read(io).tap { |image| variation.transform(image) }.path
+ 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
index 13bad87cac..da4af62666 100644
--- a/activestorage/app/models/active_storage/variation.rb
+++ b/activestorage/app/models/active_storage/variation.rb
@@ -46,11 +46,17 @@ class ActiveStorage::Variation
# 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 |(method, argument)|
- if eligible_argument?(argument)
- image.public_send(method, argument)
- else
- image.public_send(method)
+ ActiveSupport::Notifications.instrument("transform.active_storage") do
+ 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
end
@@ -61,6 +67,14 @@ class ActiveStorage::Variation
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
diff --git a/activestorage/config/routes.rb b/activestorage/config/routes.rb
index 1eae21445a..3f4129d915 100644
--- a/activestorage/config/routes.rb
+++ b/activestorage/config/routes.rb
@@ -1,17 +1,17 @@
# 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, internal: true
+ 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) }
+ 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, internal: true
+ 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
@@ -24,7 +24,7 @@ Rails.application.routes.draw do
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, internal: true
+ 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
@@ -37,7 +37,7 @@ Rails.application.routes.draw do
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, internal: true
- put "/rails/active_storage/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service, internal: true
- post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads, internal: true
+ 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/lib/active_storage.rb b/activestorage/lib/active_storage.rb
index d1ff6b7032..e1bd974853 100644
--- a/activestorage/lib/active_storage.rb
+++ b/activestorage/lib/active_storage.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#--
-# Copyright (c) 2017 David Heinemeier Hansson
+# 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
@@ -26,7 +26,11 @@
require "active_record"
require "active_support"
require "active_support/rails"
+
require "active_storage/version"
+require "active_storage/errors"
+
+require "marcel"
module ActiveStorage
extend ActiveSupport::Autoload
@@ -41,4 +45,7 @@ module ActiveStorage
mattr_accessor :queue
mattr_accessor :previewers, default: []
mattr_accessor :analyzers, default: []
+ mattr_accessor :paths, default: {}
+ mattr_accessor :variable_content_types, default: []
+ mattr_accessor :content_types_to_serve_as_binary, default: []
end
diff --git a/activestorage/lib/active_storage/analyzer.rb b/activestorage/lib/active_storage/analyzer.rb
index 837785a12b..7c4168c1a0 100644
--- a/activestorage/lib/active_storage/analyzer.rb
+++ b/activestorage/lib/active_storage/analyzer.rb
@@ -26,7 +26,7 @@ module ActiveStorage
end
private
- def logger
+ def logger #:doc:
ActiveStorage.logger
end
end
diff --git a/activestorage/lib/active_storage/analyzer/image_analyzer.rb b/activestorage/lib/active_storage/analyzer/image_analyzer.rb
index 25e0251e6e..5231168a7c 100644
--- a/activestorage/lib/active_storage/analyzer/image_analyzer.rb
+++ b/activestorage/lib/active_storage/analyzer/image_analyzer.rb
@@ -9,8 +9,7 @@ module ActiveStorage
# # => { 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.
+ # the {ImageMagick}[http://www.imagemagick.org] system library.
class Analyzer::ImageAnalyzer < Analyzer
def self.accept?(blob)
blob.image?
diff --git a/activestorage/lib/active_storage/analyzer/video_analyzer.rb b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
index 1c144baa37..656e362187 100644
--- a/activestorage/lib/active_storage/analyzer/video_analyzer.rb
+++ b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
@@ -9,41 +9,40 @@ module ActiveStorage
# * Height (pixels)
# * Duration (seconds)
# * Angle (degrees)
- # * Aspect ratio
+ # * Display aspect ratio
#
# Example:
#
# ActiveStorage::VideoAnalyzer.new(blob).metadata
- # # => { width: 640, height: 480, duration: 5.0, angle: 0, aspect_ratio: [4, 3] }
+ # # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_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.
+ # When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience.
+ #
+ # This analyzer requires the {ffmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
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
+ { width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio }.compact
end
private
def width
- rotated? ? raw_height : raw_width
+ if rotated?
+ computed_height || encoded_height
+ else
+ encoded_width
+ end
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"]
+ if rotated?
+ encoded_width
+ else
+ computed_height || encoded_height
+ end
end
def duration
@@ -54,16 +53,40 @@ module ActiveStorage
Integer(tags["rotate"]) if tags["rotate"]
end
- def aspect_ratio
+ def display_aspect_ratio
if descriptor = video_stream["display_aspect_ratio"]
- descriptor.split(":", 2).collect(&:to_i)
+ if terms = descriptor.split(":", 2)
+ numerator = Integer(terms[0])
+ denominator = Integer(terms[1])
+
+ [numerator, denominator] unless numerator == 0
+ end
end
end
+
def rotated?
angle == 90 || angle == 270
end
+ def computed_height
+ if encoded_width && display_height_scale
+ encoded_width * display_height_scale
+ end
+ end
+
+ def encoded_width
+ @encoded_width ||= Float(video_stream["width"]) if video_stream["width"]
+ end
+
+ def encoded_height
+ @encoded_height ||= Float(video_stream["height"]) if video_stream["height"]
+ end
+
+ def display_height_scale
+ @display_height_scale ||= Float(display_aspect_ratio.last) / display_aspect_ratio.first if display_aspect_ratio
+ end
+
def tags
@tags ||= video_stream["tags"] || {}
@@ -89,5 +112,9 @@ module ActiveStorage
logger.info "Skipping video analysis because ffmpeg isn't installed"
{}
end
+
+ def ffprobe_path
+ ActiveStorage.paths[:ffprobe] || "ffprobe"
+ end
end
end
diff --git a/activestorage/lib/active_storage/attached/macros.rb b/activestorage/lib/active_storage/attached/macros.rb
index 2b38a9b887..c51efa9d6b 100644
--- a/activestorage/lib/active_storage/attached/macros.rb
+++ b/activestorage/lib/active_storage/attached/macros.rb
@@ -38,13 +38,13 @@ module ActiveStorage
end
CODE
- has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record
+ has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :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 }
+ after_destroy_commit { public_send(name).purge_later }
end
end
@@ -83,13 +83,13 @@ module ActiveStorage
end
CODE
- has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment"
+ has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record
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 }
+ after_destroy_commit { public_send(name).purge_later }
end
end
end
diff --git a/activestorage/lib/active_storage/downloading.rb b/activestorage/lib/active_storage/downloading.rb
index 3dac6b116a..295289c1e7 100644
--- a/activestorage/lib/active_storage/downloading.rb
+++ b/activestorage/lib/active_storage/downloading.rb
@@ -1,25 +1,27 @@
# frozen_string_literal: true
+require "tmpdir"
+
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", tempdir) do |file|
+ 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:
+ 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:
+ def tempdir #:doc:
Dir.tmpdir
end
end
diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb
index b870e6d4d6..8ba32490b1 100644
--- a/activestorage/lib/active_storage/engine.rb
+++ b/activestorage/lib/active_storage/engine.rb
@@ -18,6 +18,18 @@ module ActiveStorage
config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ]
config.active_storage.paths = ActiveSupport::OrderedOptions.new
+ config.active_storage.variable_content_types = %w( image/png image/gif image/jpg image/jpeg image/vnd.adobe.photoshop )
+ config.active_storage.content_types_to_serve_as_binary = %w(
+ text/html
+ text/javascript
+ image/svg+xml
+ application/postscript
+ application/x-shockwave-flash
+ text/xml
+ application/xml
+ application/xhtml+xml
+ )
+
config.eager_load_namespaces << ActiveStorage
initializer "active_storage.configs" do
@@ -26,6 +38,10 @@ module ActiveStorage
ActiveStorage.queue = app.config.active_storage.queue
ActiveStorage.previewers = app.config.active_storage.previewers || []
ActiveStorage.analyzers = app.config.active_storage.analyzers || []
+ ActiveStorage.paths = app.config.active_storage.paths || {}
+
+ ActiveStorage.variable_content_types = app.config.active_storage.variable_content_types || []
+ ActiveStorage.content_types_to_serve_as_binary = app.config.active_storage.content_types_to_serve_as_binary || []
end
end
@@ -69,21 +85,5 @@ module ActiveStorage
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/errors.rb b/activestorage/lib/active_storage/errors.rb
new file mode 100644
index 0000000000..f099b13f5b
--- /dev/null
+++ b/activestorage/lib/active_storage/errors.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class InvariableError < StandardError; end
+ class UnpreviewableError < StandardError; end
+ class UnrepresentableError < StandardError; end
+end
diff --git a/activestorage/lib/active_storage/previewer.rb b/activestorage/lib/active_storage/previewer.rb
index 3d485988e9..dacab1e7df 100644
--- a/activestorage/lib/active_storage/previewer.rb
+++ b/activestorage/lib/active_storage/previewer.rb
@@ -42,20 +42,22 @@ module ActiveStorage
# 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
+ def draw(*argv) #:doc:
+ ActiveSupport::Notifications.instrument("preview.active_storage") do
+ Tempfile.open("ActiveStorage", tempdir) do |file|
+ capture(*argv, to: file)
+ yield file
+ end
end
end
def capture(*argv, to:)
to.binmode
- IO.popen(argv) { |out| IO.copy_stream(out, to) }
+ IO.popen(argv, err: File::NULL) { |out| IO.copy_stream(out, to) }
to.rewind
end
- def logger
+ def logger #:doc:
ActiveStorage.logger
end
end
diff --git a/activestorage/lib/active_storage/previewer/pdf_previewer.rb b/activestorage/lib/active_storage/previewer/pdf_previewer.rb
index b84aefcc9c..426ff51eb1 100644
--- a/activestorage/lib/active_storage/previewer/pdf_previewer.rb
+++ b/activestorage/lib/active_storage/previewer/pdf_previewer.rb
@@ -2,8 +2,6 @@
module ActiveStorage
class Previewer::PDFPreviewer < Previewer
- class_attribute :mutool_path, default: "mutool"
-
def self.accept?(blob)
blob.content_type == "application/pdf"
end
@@ -20,5 +18,9 @@ module ActiveStorage
def draw_first_page_from(file, &block)
draw mutool_path, "draw", "-F", "png", "-o", "-", file.path, "1", &block
end
+
+ def mutool_path
+ ActiveStorage.paths[:mutool] || "mutool"
+ end
end
end
diff --git a/activestorage/lib/active_storage/previewer/video_previewer.rb b/activestorage/lib/active_storage/previewer/video_previewer.rb
index 5d06e33f44..2f28a3d341 100644
--- a/activestorage/lib/active_storage/previewer/video_previewer.rb
+++ b/activestorage/lib/active_storage/previewer/video_previewer.rb
@@ -2,8 +2,6 @@
module ActiveStorage
class Previewer::VideoPreviewer < Previewer
- class_attribute :ffmpeg_path, default: "ffmpeg"
-
def self.accept?(blob)
blob.video?
end
@@ -21,5 +19,9 @@ module ActiveStorage
draw ffmpeg_path, "-i", file.path, "-y", "-vcodec", "png",
"-vf", "thumbnail", "-vframes", "1", "-f", "image2", "-", &block
end
+
+ def ffmpeg_path
+ ActiveStorage.paths[:ffmpeg] || "ffmpeg"
+ end
end
end
diff --git a/activestorage/lib/active_storage/service.rb b/activestorage/lib/active_storage/service.rb
index c8f675db86..f2e1269f27 100644
--- a/activestorage/lib/active_storage/service.rb
+++ b/activestorage/lib/active_storage/service.rb
@@ -97,7 +97,7 @@ module ActiveStorage
# 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 most also provide the +content_type+, +content_length+, and +checksum+ of the file
+ # 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
diff --git a/activestorage/lib/active_storage/service/azure_storage_service.rb b/activestorage/lib/active_storage/service/azure_storage_service.rb
index 19b09991b3..0a9eb7f23d 100644
--- a/activestorage/lib/active_storage/service/azure_storage_service.rb
+++ b/activestorage/lib/active_storage/service/azure_storage_service.rb
@@ -46,7 +46,7 @@ module ActiveStorage
begin
blobs.delete_blob(container, key)
rescue Azure::Core::Http::HTTPError
- false
+ # Ignore files already deleted
end
end
end
diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb
index a8728c5bc3..d17eea9046 100644
--- a/activestorage/lib/active_storage/service/disk_service.rb
+++ b/activestorage/lib/active_storage/service/disk_service.rb
@@ -9,10 +9,10 @@ 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
+ attr_reader :root, :host
- def initialize(root:)
- @root = root
+ def initialize(root:, host: "http://localhost:3000")
+ @root, @host = root, host
end
def upload(key, io, checksum: nil)
@@ -69,14 +69,13 @@ module ActiveStorage
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
+ Rails.application.routes.url_helpers.rails_disk_service_url(
+ verified_key_with_expiration,
+ filename: filename,
+ disposition: content_disposition_with(type: disposition, filename: filename),
+ content_type: content_type,
+ host: host
+ )
payload[:url] = generated_url
@@ -97,12 +96,7 @@ module ActiveStorage
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
+ generated_url = Rails.application.routes.url_helpers.update_rails_disk_service_url(verified_token_with_expiration, host: host)
payload[:url] = generated_url
diff --git a/activestorage/lib/tasks/activestorage.rake b/activestorage/lib/tasks/activestorage.rake
index ef923e5926..296e91afa1 100644
--- a/activestorage/lib/tasks/activestorage.rake
+++ b/activestorage/lib/tasks/activestorage.rake
@@ -3,6 +3,10 @@
namespace :active_storage do
desc "Copy over the migration needed to the application"
task install: :environment do
- Rake::Task["active_storage:install:migrations"].invoke
+ 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/test/analyzer/image_analyzer_test.rb b/activestorage/test/analyzer/image_analyzer_test.rb
index 9087072215..0d9f24c5c1 100644
--- a/activestorage/test/analyzer/image_analyzer_test.rb
+++ b/activestorage/test/analyzer/image_analyzer_test.rb
@@ -6,11 +6,19 @@ require "database/setup"
require "active_storage/analyzer/image_analyzer"
class ActiveStorage::Analyzer::ImageAnalyzerTest < ActiveSupport::TestCase
- test "analyzing an image" do
+ 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
index b3b9c97fe4..2612006551 100644
--- a/activestorage/test/analyzer/video_analyzer_test.rb
+++ b/activestorage/test/analyzer/video_analyzer_test.rb
@@ -8,28 +8,52 @@ 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
+ metadata = extract_metadata_from(blob)
assert_equal 640, metadata[:width]
assert_equal 480, metadata[:height]
- assert_equal [4, 3], metadata[:aspect_ratio]
+ assert_equal [4, 3], metadata[:display_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
+ metadata = extract_metadata_from(blob)
assert_equal 480, metadata[:width]
assert_equal 640, metadata[:height]
- assert_equal [4, 3], metadata[:aspect_ratio]
+ assert_equal [4, 3], metadata[:display_aspect_ratio]
assert_equal 5.227975, metadata[:duration]
assert_equal 90, metadata[:angle]
end
+ test "analyzing a video with rectangular samples" do
+ blob = create_file_blob(filename: "video_with_rectangular_samples.mp4", content_type: "video/mp4")
+ metadata = extract_metadata_from(blob)
+
+ assert_equal 1280, metadata[:width]
+ assert_equal 720, metadata[:height]
+ assert_equal [16, 9], metadata[:display_aspect_ratio]
+ end
+
+ test "analyzing a video with an undefined display aspect ratio" do
+ blob = create_file_blob(filename: "video_with_undefined_display_aspect_ratio.mp4", content_type: "video/mp4")
+ metadata = extract_metadata_from(blob)
+
+ assert_equal 640, metadata[:width]
+ assert_equal 480, metadata[:height]
+ assert_nil metadata[:display_aspect_ratio]
+ 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)
+ metadata = extract_metadata_from(blob)
+ assert_equal({ "analyzed" => true, "identified" => true }, metadata)
end
+
+ private
+ def extract_metadata_from(blob)
+ blob.tap(&:analyze).metadata
+ end
end
diff --git a/activestorage/test/controllers/blobs_controller_test.rb b/activestorage/test/controllers/blobs_controller_test.rb
index 97177e64c2..9c811df895 100644
--- a/activestorage/test/controllers/blobs_controller_test.rb
+++ b/activestorage/test/controllers/blobs_controller_test.rb
@@ -8,6 +8,11 @@ class ActiveStorage::BlobsControllerTest < ActionDispatch::IntegrationTest
@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)
diff --git a/activestorage/test/controllers/previews_controller_test.rb b/activestorage/test/controllers/previews_controller_test.rb
index c3151a710e..704a466160 100644
--- a/activestorage/test/controllers/previews_controller_test.rb
+++ b/activestorage/test/controllers/previews_controller_test.rb
@@ -21,4 +21,13 @@ class ActiveStorage::PreviewsControllerTest < ActionDispatch::IntegrationTest
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
index 6c70d73786..a0642f9bed 100644
--- a/activestorage/test/controllers/variants_controller_test.rb
+++ b/activestorage/test/controllers/variants_controller_test.rb
@@ -20,4 +20,13 @@ class ActiveStorage::VariantsControllerTest < ActionDispatch::IntegrationTest
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/setup.rb b/activestorage/test/database/setup.rb
index 705650a25d..daeeb5695b 100644
--- a/activestorage/test/database/setup.rb
+++ b/activestorage/test/database/setup.rb
@@ -3,5 +3,5 @@
require_relative "create_users_migration"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
-ActiveRecord::Migrator.migrate File.expand_path("../../db/migrate", __dir__)
+ActiveRecord::Base.connection.migration_context.migrate
ActiveStorageCreateUsers.migrate(:up)
diff --git a/activestorage/test/dummy/config/application.rb b/activestorage/test/dummy/config/application.rb
index 06cbc453a2..bd14ac0b1a 100644
--- a/activestorage/test/dummy/config/application.rb
+++ b/activestorage/test/dummy/config/application.rb
@@ -10,10 +10,6 @@ require "action_controller/railtie"
require "action_view/railtie"
require "sprockets/railtie"
require "active_storage/engine"
-#require "action_mailer/railtie"
-#require "rails/test_unit/railtie"
-#require "action_cable/engine"
-
Bundler.require(*Rails.groups)
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/video_with_rectangular_samples.mp4 b/activestorage/test/fixtures/files/video_with_rectangular_samples.mp4
new file mode 100644
index 0000000000..12b04afc87
--- /dev/null
+++ b/activestorage/test/fixtures/files/video_with_rectangular_samples.mp4
Binary files differ
diff --git a/activestorage/test/fixtures/files/video_with_undefined_display_aspect_ratio.mp4 b/activestorage/test/fixtures/files/video_with_undefined_display_aspect_ratio.mp4
new file mode 100644
index 0000000000..eb354e756f
--- /dev/null
+++ b/activestorage/test/fixtures/files/video_with_undefined_display_aspect_ratio.mp4
Binary files differ
diff --git a/activestorage/test/models/attachments_test.rb b/activestorage/test/models/attachments_test.rb
index 20eec3c220..f0aa96b411 100644
--- a/activestorage/test/models/attachments_test.rb
+++ b/activestorage/test/models/attachments_test.rb
@@ -97,6 +97,29 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
assert_equal "funky.jpg", @user.avatar_attachment.blob.filename.to_s
end
+ test "identify newly-attached, directly-uploaded blob" do
+ # Simulate a direct upload.
+ blob = create_blob_before_direct_upload(filename: "racecar.jpg", content_type: "application/octet-stream", byte_size: 1124062, checksum: "7GjDDNEQb4mzMzsW+MS0JQ==")
+ ActiveStorage::Blob.service.upload(blob.key, file_fixture("racecar.jpg").open)
+
+ stub_request(:get, %r{localhost:3000/rails/active_storage/disk/.*}).to_return(body: file_fixture("racecar.jpg"))
+ @user.avatar.attach(blob)
+
+ assert_equal "image/jpeg", @user.avatar.reload.content_type
+ assert @user.avatar.identified?
+ end
+
+ test "identify newly-attached blob only once" do
+ blob = create_file_blob
+ assert blob.identified?
+
+ # The blob's backing file is a PNG image. Fudge its content type so we can tell if it's identified when we attach it.
+ blob.update! content_type: "application/octet-stream"
+
+ @user.avatar.attach blob
+ assert_equal "application/octet-stream", blob.content_type
+ end
+
test "analyze newly-attached blob" do
perform_enqueued_jobs do
@user.avatar.attach create_file_blob
@@ -115,7 +138,7 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
assert blob.reload.analyzed?
- @user.avatar.attachment.destroy
+ @user.avatar.detach
assert_no_enqueued_jobs do
@user.reload.avatar.attach blob
diff --git a/activestorage/test/models/blob_test.rb b/activestorage/test/models/blob_test.rb
index f94e65ed77..b5daee2b57 100644
--- a/activestorage/test/models/blob_test.rb
+++ b/activestorage/test/models/blob_test.rb
@@ -13,6 +13,16 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase
assert_equal Digest::MD5.base64digest(data), blob.checksum
end
+ test "create after upload extracts content type from data" do
+ blob = create_file_blob content_type: "application/octet-stream"
+ assert_equal "image/jpeg", blob.content_type
+ end
+
+ test "create after upload extracts content type from filename" do
+ blob = create_blob content_type: "application/octet-stream"
+ assert_equal "text/plain", blob.content_type
+ end
+
test "text?" do
blob = create_blob data: "Hello world!"
assert blob.text?
@@ -41,6 +51,25 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase
end
end
+ test "urls force attachment as content disposition for content types served as binary" do
+ blob = create_blob(content_type: "text/html")
+
+ freeze_time do
+ assert_equal expected_url_for(blob, disposition: :attachment), blob.service_url
+ assert_equal expected_url_for(blob, disposition: :attachment), blob.service_url(disposition: :inline)
+ end
+ end
+
+ test "urls allow for custom filename" do
+ blob = create_blob(filename: "original.txt")
+ new_filename = ActiveStorage::Filename.new("new.txt")
+
+ freeze_time do
+ assert_equal expected_url_for(blob), blob.service_url
+ assert_equal expected_url_for(blob, filename: new_filename), blob.service_url(filename: new_filename)
+ end
+ end
+
test "purge deletes file from external service" do
blob = create_blob
@@ -57,8 +86,9 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase
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}"
+ def expected_url_for(blob, disposition: :inline, filename: nil)
+ filename ||= blob.filename
+ query_string = { content_type: blob.content_type, disposition: "#{disposition}; #{filename.parameters}" }.to_param
+ "http://localhost:3000/rails/active_storage/disk/#{ActiveStorage.verifier.generate(blob.key, expires_in: 5.minutes, purpose: :blob_key)}/#{filename}?#{query_string}"
end
end
diff --git a/activestorage/test/models/preview_test.rb b/activestorage/test/models/preview_test.rb
index bcd8442f4b..b0c6d2c45b 100644
--- a/activestorage/test/models/preview_test.rb
+++ b/activestorage/test/models/preview_test.rb
@@ -33,7 +33,7 @@ class ActiveStorage::PreviewTest < ActiveSupport::TestCase
test "previewing an unpreviewable blob" do
blob = create_file_blob
- assert_raises ActiveStorage::Blob::UnpreviewableError do
+ assert_raises ActiveStorage::UnpreviewableError do
blob.preview resize: "640x280"
end
end
diff --git a/activestorage/test/models/representation_test.rb b/activestorage/test/models/representation_test.rb
index 29fe61aee4..2a06b31c77 100644
--- a/activestorage/test/models/representation_test.rb
+++ b/activestorage/test/models/representation_test.rb
@@ -34,7 +34,7 @@ class ActiveStorage::RepresentationTest < ActiveSupport::TestCase
test "representing an unrepresentable blob" do
blob = create_blob
- assert_raises ActiveStorage::Blob::UnrepresentableError do
+ assert_raises ActiveStorage::UnrepresentableError do
blob.representation resize: "100x100"
end
end
diff --git a/activestorage/test/models/variant_test.rb b/activestorage/test/models/variant_test.rb
index b7d20ab55a..0cf8a583bd 100644
--- a/activestorage/test/models/variant_test.rb
+++ b/activestorage/test/models/variant_test.rb
@@ -4,12 +4,9 @@ require "test_helper"
require "database/setup"
class ActiveStorage::VariantTest < ActiveSupport::TestCase
- setup do
- @blob = create_file_blob filename: "racecar.jpg"
- end
-
- test "resized variation" do
- variant = @blob.variant(resize: "100x100").processed
+ 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)
@@ -17,8 +14,9 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase
assert_equal 67, image.height
end
- test "resized and monochrome variation" do
- variant = @blob.variant(resize: "100x100", monochrome: true).processed
+ 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)
@@ -27,8 +25,48 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase
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::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
- variant = @blob.variant(font: "a" * 10_000).processed
- assert_operator variant.service_url.length, :<, 500
+ blob = create_file_blob(filename: "racecar.jpg")
+ variant = blob.variant(font: "a" * 10_000).processed
+ assert_operator variant.service_url.length, :<, 525
end
end
diff --git a/activestorage/test/service/configurator_test.rb b/activestorage/test/service/configurator_test.rb
index a2fd035e02..fe8a637ad0 100644
--- a/activestorage/test/service/configurator_test.rb
+++ b/activestorage/test/service/configurator_test.rb
@@ -5,7 +5,10 @@ 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
+ assert_equal "path", service.root
+ assert_equal "http://localhost:3000", service.host
end
test "raises error when passing non-existent service name" do
diff --git a/activestorage/test/service/mirror_service_test.rb b/activestorage/test/service/mirror_service_test.rb
index 92101b1282..08efb095bc 100644
--- a/activestorage/test/service/mirror_service_test.rb
+++ b/activestorage/test/service/mirror_service_test.rb
@@ -10,8 +10,8 @@ class ActiveStorage::Service::MirrorServiceTest < ActiveSupport::TestCase
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") }
+ 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
diff --git a/activestorage/test/test_helper.rb b/activestorage/test/test_helper.rb
index aaf1d452ea..98fa44a604 100644
--- a/activestorage/test/test_helper.rb
+++ b/activestorage/test/test_helper.rb
@@ -7,6 +7,7 @@ require "bundler/setup"
require "active_support"
require "active_support/test_case"
require "active_support/testing/autorun"
+require "webmock/minitest"
require "mini_magick"
begin
@@ -41,6 +42,8 @@ ActiveStorage.verifier = ActiveSupport::MessageVerifier.new("Testing")
class ActiveSupport::TestCase
self.file_fixture_path = File.expand_path("fixtures/files", __dir__)
+ setup { WebMock.allow_net_connect! }
+
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
diff --git a/activestorage/webpack.config.js b/activestorage/webpack.config.js
index 92c4530e7f..3a50eef470 100644
--- a/activestorage/webpack.config.js
+++ b/activestorage/webpack.config.js
@@ -1,4 +1,3 @@
-const webpack = require("webpack")
const path = require("path")
module.exports = {