aboutsummaryrefslogtreecommitdiffstats
path: root/activestorage
diff options
context:
space:
mode:
Diffstat (limited to 'activestorage')
-rw-r--r--activestorage/.gitignore12
-rw-r--r--activestorage/CHANGELOG.md3
-rw-r--r--activestorage/Rakefile1
-rw-r--r--activestorage/activestorage.gemspec4
-rw-r--r--activestorage/app/assets/javascripts/activestorage.js2
-rw-r--r--activestorage/app/controllers/active_storage/base_controller.rb10
-rw-r--r--activestorage/app/controllers/active_storage/blobs_controller.rb2
-rw-r--r--activestorage/app/controllers/active_storage/direct_uploads_controller.rb4
-rw-r--r--activestorage/app/controllers/active_storage/disk_controller.rb4
-rw-r--r--activestorage/app/controllers/active_storage/previews_controller.rb10
-rw-r--r--activestorage/app/controllers/active_storage/representations_controller.rb (renamed from activestorage/app/controllers/active_storage/variants_controller.rb)6
-rw-r--r--activestorage/app/javascript/activestorage/blob_upload.js2
-rw-r--r--activestorage/app/javascript/activestorage/direct_upload.js6
-rw-r--r--activestorage/app/javascript/activestorage/ujs.js2
-rw-r--r--activestorage/app/models/active_storage/attachment.rb2
-rw-r--r--activestorage/app/models/active_storage/blob.rb16
-rw-r--r--activestorage/app/models/active_storage/blob/identifiable.rb11
-rw-r--r--activestorage/app/models/active_storage/blob/representable.rb6
-rw-r--r--activestorage/app/models/active_storage/current.rb5
-rw-r--r--activestorage/app/models/active_storage/filename.rb10
-rw-r--r--activestorage/app/models/active_storage/identification.rb38
-rw-r--r--activestorage/app/models/active_storage/preview.rb7
-rw-r--r--activestorage/app/models/active_storage/variant.rb8
-rw-r--r--activestorage/app/models/active_storage/variation.rb8
-rw-r--r--activestorage/config/routes.rb28
-rw-r--r--activestorage/lib/active_storage/analyzer/video_analyzer.rb2
-rw-r--r--activestorage/lib/active_storage/attached/macros.rb18
-rw-r--r--activestorage/lib/active_storage/attached/many.rb18
-rw-r--r--activestorage/lib/active_storage/attached/one.rb27
-rw-r--r--activestorage/lib/active_storage/engine.rb17
-rw-r--r--activestorage/lib/active_storage/previewer/mupdf_previewer.rb36
-rw-r--r--activestorage/lib/active_storage/previewer/pdf_previewer.rb26
-rw-r--r--activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb35
-rw-r--r--activestorage/lib/active_storage/service.rb5
-rw-r--r--activestorage/lib/active_storage/service/azure_storage_service.rb29
-rw-r--r--activestorage/lib/active_storage/service/disk_service.rb31
-rw-r--r--activestorage/lib/active_storage/service/gcs_service.rb22
-rw-r--r--activestorage/lib/active_storage/service/mirror_service.rb2
-rw-r--r--activestorage/lib/active_storage/service/s3_service.rb10
-rw-r--r--activestorage/package.json10
-rw-r--r--activestorage/test/controllers/direct_uploads_controller_test.rb27
-rw-r--r--activestorage/test/controllers/previews_controller_test.rb33
-rw-r--r--activestorage/test/controllers/representations_controller_test.rb61
-rw-r--r--activestorage/test/controllers/variants_controller_test.rb32
-rw-r--r--activestorage/test/fixtures/files/favicon.icobin0 -> 16958 bytes
-rw-r--r--activestorage/test/models/attachments_test.rb101
-rw-r--r--activestorage/test/models/blob_test.rb21
-rw-r--r--activestorage/test/models/variant_test.rb11
-rw-r--r--activestorage/test/previewer/mupdf_previewer_test.rb (renamed from activestorage/test/previewer/pdf_previewer_test.rb)6
-rw-r--r--activestorage/test/previewer/poppler_pdf_previewer_test.rb23
-rw-r--r--activestorage/test/service/configurations.example.yml1
-rw-r--r--activestorage/test/service/configurations.yml.encbin2864 -> 2848 bytes
-rw-r--r--activestorage/test/service/configurator_test.rb2
-rw-r--r--activestorage/test/service/disk_service_test.rb2
-rw-r--r--activestorage/test/service/gcs_service_test.rb2
-rw-r--r--activestorage/test/service/s3_service_test.rb4
-rw-r--r--activestorage/test/service/shared_service_tests.rb5
-rw-r--r--activestorage/test/test_helper.rb22
58 files changed, 572 insertions, 276 deletions
diff --git a/activestorage/.gitignore b/activestorage/.gitignore
index a532335bdd..3e78878ffc 100644
--- a/activestorage/.gitignore
+++ b/activestorage/.gitignore
@@ -1,6 +1,6 @@
-.byebug_history
-node_modules
-test/dummy/db/*.sqlite3
-test/dummy/db/*.sqlite3-journal
-test/dummy/log/*.log
-test/dummy/tmp/
+/src/
+/test/dummy/db/*.sqlite3
+/test/dummy/db/*.sqlite3-journal
+/test/dummy/log/*.log
+/test/dummy/tmp/
+/test/service/configurations.yml
diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md
index 6354ab9924..d794afb0e6 100644
--- a/activestorage/CHANGELOG.md
+++ b/activestorage/CHANGELOG.md
@@ -1,3 +1,6 @@
+* Rails 6 requires Ruby 2.4.1 or newer.
+
+ *Jeremy Daer*
Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activestorage/CHANGELOG.md) for previous changes.
diff --git a/activestorage/Rakefile b/activestorage/Rakefile
index 2aa4d2a76f..7dc69e04ea 100644
--- a/activestorage/Rakefile
+++ b/activestorage/Rakefile
@@ -8,6 +8,7 @@ Rake::TestTask.new do |test|
test.libs << "app/controllers"
test.libs << "test"
test.test_files = FileList["test/**/*_test.rb"]
+ test.verbose = true
test.warning = false
end
diff --git a/activestorage/activestorage.gemspec b/activestorage/activestorage.gemspec
index d135324700..cb1bb00a25 100644
--- a/activestorage/activestorage.gemspec
+++ b/activestorage/activestorage.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "Local and cloud file storage framework."
s.description = "Attach cloud and local files in Rails applications."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
@@ -29,6 +29,4 @@ Gem::Specification.new do |s|
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 a2f06c43a3..3e1fbc7b50 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=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
+!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){if(r)return void t(r);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.slice())}},{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}()}])});
diff --git a/activestorage/app/controllers/active_storage/base_controller.rb b/activestorage/app/controllers/active_storage/base_controller.rb
new file mode 100644
index 0000000000..59312ac8df
--- /dev/null
+++ b/activestorage/app/controllers/active_storage/base_controller.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+# The base controller for all ActiveStorage controllers.
+class ActiveStorage::BaseController < ActionController::Base
+ protect_from_forgery with: :exception
+
+ before_action do
+ ActiveStorage::Current.host = request.base_url
+ end
+end
diff --git a/activestorage/app/controllers/active_storage/blobs_controller.rb b/activestorage/app/controllers/active_storage/blobs_controller.rb
index fa44131048..92e54c386d 100644
--- a/activestorage/app/controllers/active_storage/blobs_controller.rb
+++ b/activestorage/app/controllers/active_storage/blobs_controller.rb
@@ -4,7 +4,7 @@
# Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
# security-through-obscurity factor of the signed blob references, you'll need to implement your own
# authenticated redirection controller.
-class ActiveStorage::BlobsController < ActionController::Base
+class ActiveStorage::BlobsController < ActiveStorage::BaseController
include ActiveStorage::SetBlob
def show
diff --git a/activestorage/app/controllers/active_storage/direct_uploads_controller.rb b/activestorage/app/controllers/active_storage/direct_uploads_controller.rb
index 205d173648..78b43fc94c 100644
--- a/activestorage/app/controllers/active_storage/direct_uploads_controller.rb
+++ b/activestorage/app/controllers/active_storage/direct_uploads_controller.rb
@@ -3,7 +3,7 @@
# Creates a new blob on the server side in anticipation of a direct-to-service upload from the client side.
# When the client-side upload is completed, the signed_blob_id can be submitted as part of the form to reference
# the blob that was created up front.
-class ActiveStorage::DirectUploadsController < ActionController::Base
+class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
def create
blob = ActiveStorage::Blob.create_before_direct_upload!(blob_args)
render json: direct_upload_json(blob)
@@ -15,7 +15,7 @@ class ActiveStorage::DirectUploadsController < ActionController::Base
end
def direct_upload_json(blob)
- blob.as_json(methods: :signed_id).merge(direct_upload: {
+ blob.as_json(root: false, methods: :signed_id).merge(direct_upload: {
url: blob.service_url_for_direct_upload,
headers: blob.service_headers_for_direct_upload
})
diff --git a/activestorage/app/controllers/active_storage/disk_controller.rb b/activestorage/app/controllers/active_storage/disk_controller.rb
index a7e10c0696..7bc5eb3fdb 100644
--- a/activestorage/app/controllers/active_storage/disk_controller.rb
+++ b/activestorage/app/controllers/active_storage/disk_controller.rb
@@ -4,8 +4,8 @@
# This means using expiring, signed URLs that are meant for immediate access, not permanent linking.
# Always go through the BlobsController, or your own authenticated controller, rather than directly
# to the service url.
-class ActiveStorage::DiskController < ActionController::Base
- skip_forgery_protection if default_protect_from_forgery
+class ActiveStorage::DiskController < ActiveStorage::BaseController
+ skip_forgery_protection
def show
if key = decode_verified_key
diff --git a/activestorage/app/controllers/active_storage/previews_controller.rb b/activestorage/app/controllers/active_storage/previews_controller.rb
deleted file mode 100644
index aa7ef58ca4..0000000000
--- a/activestorage/app/controllers/active_storage/previews_controller.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-class ActiveStorage::PreviewsController < ActionController::Base
- include ActiveStorage::SetBlob
-
- def show
- expires_in ActiveStorage::Blob.service.url_expires_in
- redirect_to ActiveStorage::Preview.new(@blob, params[:variation_key]).processed.service_url(disposition: params[:disposition])
- end
-end
diff --git a/activestorage/app/controllers/active_storage/variants_controller.rb b/activestorage/app/controllers/active_storage/representations_controller.rb
index e8f8dd592d..ce9286db7d 100644
--- a/activestorage/app/controllers/active_storage/variants_controller.rb
+++ b/activestorage/app/controllers/active_storage/representations_controller.rb
@@ -1,14 +1,14 @@
# frozen_string_literal: true
-# Take a signed permanent reference for a variant and turn it into an expiring service URL for download.
+# Take a signed permanent reference for a blob representation and turn it into an expiring service URL for download.
# Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
# security-through-obscurity factor of the signed blob and variation reference, you'll need to implement your own
# authenticated redirection controller.
-class ActiveStorage::VariantsController < ActionController::Base
+class ActiveStorage::RepresentationsController < ActiveStorage::BaseController
include ActiveStorage::SetBlob
def show
expires_in ActiveStorage::Blob.service.url_expires_in
- redirect_to ActiveStorage::Variant.new(@blob, params[:variation_key]).processed.service_url(disposition: params[:disposition])
+ redirect_to @blob.representation(params[:variation_key]).processed.service_url(disposition: params[:disposition])
end
end
diff --git a/activestorage/app/javascript/activestorage/blob_upload.js b/activestorage/app/javascript/activestorage/blob_upload.js
index 180a7415e7..277cc8ff8e 100644
--- a/activestorage/app/javascript/activestorage/blob_upload.js
+++ b/activestorage/app/javascript/activestorage/blob_upload.js
@@ -17,7 +17,7 @@ export class BlobUpload {
create(callback) {
this.callback = callback
- this.xhr.send(this.file)
+ this.xhr.send(this.file.slice())
}
requestDidLoad(event) {
diff --git a/activestorage/app/javascript/activestorage/direct_upload.js b/activestorage/app/javascript/activestorage/direct_upload.js
index 7085e0a4ab..c2eedf289b 100644
--- a/activestorage/app/javascript/activestorage/direct_upload.js
+++ b/activestorage/app/javascript/activestorage/direct_upload.js
@@ -14,8 +14,14 @@ export class DirectUpload {
create(callback) {
FileChecksum.create(this.file, (error, checksum) => {
+ if (error) {
+ callback(error)
+ return
+ }
+
const blob = new BlobRecord(this.file, checksum, this.url)
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr)
+
blob.create(error => {
if (error) {
callback(error)
diff --git a/activestorage/app/javascript/activestorage/ujs.js b/activestorage/app/javascript/activestorage/ujs.js
index 1dda02936f..08c535470d 100644
--- a/activestorage/app/javascript/activestorage/ujs.js
+++ b/activestorage/app/javascript/activestorage/ujs.js
@@ -59,7 +59,7 @@ function submitForm(form) {
} else {
button = document.createElement("input")
button.type = "submit"
- button.style = "display:none"
+ button.style.display = "none"
form.appendChild(button)
button.click()
form.removeChild(button)
diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb
index 19f48c57d6..c59877a9a5 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 :identify_blob, :analyze_blob_later
+ after_create_commit :analyze_blob_later, :identify_blob
# Synchronously purges the blob (deletes it from the configured service) and destroys the attachment.
def purge
diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb
index 892a833fae..0cd4ad8128 100644
--- a/activestorage/app/models/active_storage/blob.rb
+++ b/activestorage/app/models/active_storage/blob.rb
@@ -14,7 +14,13 @@
# 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
- include Analyzable, Identifiable, Representable
+ require_dependency "active_storage/blob/analyzable"
+ require_dependency "active_storage/blob/identifiable"
+ require_dependency "active_storage/blob/representable"
+
+ include Analyzable
+ include Identifiable
+ include Representable
self.table_name = "active_storage_blobs"
@@ -25,6 +31,8 @@ class ActiveStorage::Blob < ActiveRecord::Base
has_many :attachments
+ scope :unattached, -> { left_joins(:attachments).where(ActiveStorage::Attachment.table_name => { blob_id: nil }) }
+
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
@@ -109,7 +117,9 @@ class ActiveStorage::Blob < ActiveRecord::Base
# 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, filename: self.filename, **options)
+ def service_url(expires_in: service.url_expires_in, disposition: :inline, filename: nil, **options)
+ filename = ActiveStorage::Filename.wrap(filename || self.filename)
+
service.url key, expires_in: expires_in, filename: filename, content_type: content_type,
disposition: forcibly_serve_as_binary? ? :attachment : disposition, **options
end
@@ -192,4 +202,6 @@ class ActiveStorage::Blob < ActiveRecord::Base
def forcibly_serve_as_binary?
ActiveStorage.content_types_to_serve_as_binary.include?(content_type)
end
+
+ ActiveSupport.run_load_hooks(:active_storage_blob, self)
end
diff --git a/activestorage/app/models/active_storage/blob/identifiable.rb b/activestorage/app/models/active_storage/blob/identifiable.rb
index 40ca84ac70..049e45dc3e 100644
--- a/activestorage/app/models/active_storage/blob/identifiable.rb
+++ b/activestorage/app/models/active_storage/blob/identifiable.rb
@@ -2,10 +2,19 @@
module ActiveStorage::Blob::Identifiable
def identify
- ActiveStorage::Identification.new(self).apply
+ update! content_type: identify_content_type, identified: true unless identified?
end
def identified?
identified
end
+
+ private
+ def identify_content_type
+ Marcel::MimeType.for download_identifiable_chunk, name: filename.to_s, declared_type: content_type
+ end
+
+ def download_identifiable_chunk
+ service.download_chunk key, 0...4.kilobytes
+ end
end
diff --git a/activestorage/app/models/active_storage/blob/representable.rb b/activestorage/app/models/active_storage/blob/representable.rb
index 0ad2e2fd77..fea62e62de 100644
--- a/activestorage/app/models/active_storage/blob/representable.rb
+++ b/activestorage/app/models/active_storage/blob/representable.rb
@@ -20,14 +20,14 @@ module ActiveStorage::Blob::Representable
#
# <%= 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
+ # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
# 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))
+ ActiveStorage::Variant.new(self, transformations)
else
raise ActiveStorage::InvariableError
end
@@ -55,7 +55,7 @@ module ActiveStorage::Blob::Representable
# 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))
+ ActiveStorage::Preview.new(self, transformations)
else
raise ActiveStorage::UnpreviewableError
end
diff --git a/activestorage/app/models/active_storage/current.rb b/activestorage/app/models/active_storage/current.rb
new file mode 100644
index 0000000000..7e431d8462
--- /dev/null
+++ b/activestorage/app/models/active_storage/current.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ActiveStorage::Current < ActiveSupport::CurrentAttributes #:nodoc:
+ attribute :host
+end
diff --git a/activestorage/app/models/active_storage/filename.rb b/activestorage/app/models/active_storage/filename.rb
index b9413dec95..bebb5e61b3 100644
--- a/activestorage/app/models/active_storage/filename.rb
+++ b/activestorage/app/models/active_storage/filename.rb
@@ -3,8 +3,18 @@
# Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization.
# A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting.
class ActiveStorage::Filename
+ require_dependency "active_storage/filename/parameters"
+
include Comparable
+ class << self
+ # Returns a Filename instance based on the given filename. If the filename is a Filename, it is
+ # returned unmodified. If it is a String, it is passed to ActiveStorage::Filename.new.
+ def wrap(filename)
+ filename.kind_of?(self) ? filename : new(filename)
+ end
+ end
+
def initialize(filename)
@filename = filename
end
diff --git a/activestorage/app/models/active_storage/identification.rb b/activestorage/app/models/active_storage/identification.rb
deleted file mode 100644
index 4f295257ae..0000000000
--- a/activestorage/app/models/active_storage/identification.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# 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/preview.rb b/activestorage/app/models/active_storage/preview.rb
index be5053edae..2b87897183 100644
--- a/activestorage/app/models/active_storage/preview.rb
+++ b/activestorage/app/models/active_storage/preview.rb
@@ -21,10 +21,9 @@
#
# Outside of a Rails application, modify +ActiveStorage.previewers+ instead.
#
-# The built-in previewers rely on third-party system libraries:
-#
-# * {ffmpeg}[https://www.ffmpeg.org]
-# * {mupdf}[https://mupdf.com]
+# The built-in previewers rely on third-party system libraries. Specifically, the built-in video previewer requires
+# {ffmpeg}[https://www.ffmpeg.org]. Two PDF previewers are provided: one requires {Poppler}[https://poppler.freedesktop.org],
+# and the other requires {mupdf}[https://mupdf.com] (version 1.8 or newer). To preview PDFs, install either Poppler or mupdf.
#
# These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you
# install and use third-party software, make sure you understand the licensing implications of doing so.
diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb
index e08a2271ec..d84208419c 100644
--- a/activestorage/app/models/active_storage/variant.rb
+++ b/activestorage/app/models/active_storage/variant.rb
@@ -13,14 +13,14 @@ require "active_storage/downloading"
# into memory. The larger the image, the more memory is used. Because of this process, you also want to be
# considerate about when the variant is actually processed. You shouldn't be processing variants inline in a
# template, for example. Delay the processing to an on-demand controller, like the one provided in
-# ActiveStorage::VariantsController.
+# ActiveStorage::RepresentationsController.
#
# To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided
# by Active Storage like so:
#
# <%= image_tag Current.user.avatar.variant(resize: "100x100") %>
#
-# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController
+# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
# can then produce on-demand.
#
# When you do want to actually produce the variant needed, call +processed+. This will check that the variant
@@ -65,7 +65,7 @@ class ActiveStorage::Variant
# it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
#
# Use <tt>url_for(variant)</tt> (or the implied form, like +link_to variant+ or +redirect_to variant+) to get the stable URL
- # for a variant that points to the ActiveStorage::VariantsController, which in turn will use this +service_call+ method
+ # for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
# for its redirection.
def service_url(expires_in: service.url_expires_in, disposition: :inline)
service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
@@ -115,7 +115,7 @@ class ActiveStorage::Variant
def download_image
require "mini_magick"
- MiniMagick::Image.create { |file| download_blob_to(file) }
+ MiniMagick::Image.create(blob.filename.extension_with_delimiter) { |file| download_blob_to(file) }
end
def transform(image)
diff --git a/activestorage/app/models/active_storage/variation.rb b/activestorage/app/models/active_storage/variation.rb
index da4af62666..12e7f9f0b5 100644
--- a/activestorage/app/models/active_storage/variation.rb
+++ b/activestorage/app/models/active_storage/variation.rb
@@ -8,6 +8,14 @@
#
# ActiveStorage::Variation.new(resize: "100x100", monochrome: true, trim: true, rotate: "-90")
#
+# You can also combine multiple transformations in one step, e.g. for center-weighted cropping:
+#
+# ActiveStorage::Variation.new(combine_options: {
+# resize: "100x100^",
+# gravity: "center",
+# crop: "100x100+0+0",
+# })
+#
# A list of all possible transformations is available at https://www.imagemagick.org/script/mogrify.php.
class ActiveStorage::Variation
attr_reader :transformations
diff --git a/activestorage/config/routes.rb b/activestorage/config/routes.rb
index 3f4129d915..20d19f334a 100644
--- a/activestorage/config/routes.rb
+++ b/activestorage/config/routes.rb
@@ -11,30 +11,18 @@ Rails.application.routes.draw do
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
+ get "/rails/active_storage/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations#show", as: :rails_blob_representation
- direct :rails_variant do |variant, options|
- signed_blob_id = variant.blob.signed_id
- variation_key = variant.variation.key
- filename = variant.blob.filename
+ direct :rails_representation do |representation, options|
+ signed_blob_id = representation.blob.signed_id
+ variation_key = representation.variation.key
+ filename = representation.blob.filename
- route_for(:rails_blob_variation, signed_blob_id, variation_key, filename, options)
+ route_for(:rails_blob_representation, signed_blob_id, variation_key, filename, options)
end
- resolve("ActiveStorage::Variant") { |variant, options| route_for(:rails_variant, variant, options) }
-
-
- get "/rails/active_storage/previews/:signed_blob_id/:variation_key/*filename" => "active_storage/previews#show", as: :rails_blob_preview
-
- direct :rails_preview do |preview, options|
- signed_blob_id = preview.blob.signed_id
- variation_key = preview.variation.key
- filename = preview.blob.filename
-
- route_for(:rails_blob_preview, signed_blob_id, variation_key, filename, options)
- end
-
- resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_preview, preview, options) }
+ resolve("ActiveStorage::Variant") { |variant, options| route_for(:rails_representation, variant, options) }
+ resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_representation, preview, options) }
get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
diff --git a/activestorage/lib/active_storage/analyzer/video_analyzer.rb b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
index 656e362187..e31bdb0edb 100644
--- a/activestorage/lib/active_storage/analyzer/video_analyzer.rb
+++ b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/core_ext/hash/compact"
-
module ActiveStorage
# Extracts the following from a video blob:
#
diff --git a/activestorage/lib/active_storage/attached/macros.rb b/activestorage/lib/active_storage/attached/macros.rb
index c51efa9d6b..819f00cc06 100644
--- a/activestorage/lib/active_storage/attached/macros.rb
+++ b/activestorage/lib/active_storage/attached/macros.rb
@@ -38,13 +38,15 @@ module ActiveStorage
end
CODE
- has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record
+ has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: false
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
after_destroy_commit { public_send(name).purge_later }
+ else
+ before_destroy { public_send(name).detach }
end
end
@@ -83,13 +85,25 @@ module ActiveStorage
end
CODE
- has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record
+ has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record, dependent: false do
+ def purge
+ each(&:purge)
+ reset
+ end
+
+ def purge_later
+ each(&:purge_later)
+ reset
+ end
+ end
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
after_destroy_commit { public_send(name).purge_later }
+ else
+ before_destroy { public_send(name).detach }
end
end
end
diff --git a/activestorage/lib/active_storage/attached/many.rb b/activestorage/lib/active_storage/attached/many.rb
index 6eace65b79..d61acb6fad 100644
--- a/activestorage/lib/active_storage/attached/many.rb
+++ b/activestorage/lib/active_storage/attached/many.rb
@@ -44,20 +44,16 @@ module ActiveStorage
attachments.destroy_all if attached?
end
+ ##
+ # :method: purge
+ #
# Directly purges each associated attachment (i.e. destroys the blobs and
# attachments and deletes the files on the service).
- def purge
- if attached?
- attachments.each(&:purge)
- attachments.reload
- end
- end
+
+ ##
+ # :method: purge_later
+ #
# Purges each associated attachment through the queuing system.
- def purge_later
- if attached?
- attachments.each(&:purge_later)
- end
- end
end
end
diff --git a/activestorage/lib/active_storage/attached/one.rb b/activestorage/lib/active_storage/attached/one.rb
index 0244232b2c..a582ed0137 100644
--- a/activestorage/lib/active_storage/attached/one.rb
+++ b/activestorage/lib/active_storage/attached/one.rb
@@ -20,10 +20,16 @@ module ActiveStorage
# person.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg")
# person.avatar.attach(avatar_blob) # ActiveStorage::Blob object
def attach(attachable)
- if attached? && dependent == :purge_later
- replace attachable
- else
- write_attachment build_attachment_from(attachable)
+ blob_was = blob if attached?
+ blob = create_blob_from(attachable)
+
+ unless blob == blob_was
+ transaction do
+ detach
+ write_attachment build_attachment(blob: blob)
+ end
+
+ blob_was.purge_later if blob_was && dependent == :purge_later
end
end
@@ -63,17 +69,10 @@ module ActiveStorage
end
private
- def replace(attachable)
- blob.tap do
- transaction do
- detach
- write_attachment build_attachment_from(attachable)
- end
- end.purge_later
- end
+ delegate :transaction, to: :record
- def build_attachment_from(attachable)
- ActiveStorage::Attachment.new(record: record, name: name, blob: create_blob_from(attachable))
+ def build_attachment(blob:)
+ ActiveStorage::Attachment.new(record: record, name: name, blob: blob)
end
def write_attachment(attachment)
diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb
index 8ba32490b1..1385e2aa84 100644
--- a/activestorage/lib/active_storage/engine.rb
+++ b/activestorage/lib/active_storage/engine.rb
@@ -3,7 +3,8 @@
require "rails"
require "active_storage"
-require "active_storage/previewer/pdf_previewer"
+require "active_storage/previewer/poppler_pdf_previewer"
+require "active_storage/previewer/mupdf_previewer"
require "active_storage/previewer/video_previewer"
require "active_storage/analyzer/image_analyzer"
@@ -14,11 +15,19 @@ module ActiveStorage
isolate_namespace ActiveStorage
config.active_storage = ActiveSupport::OrderedOptions.new
- config.active_storage.previewers = [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
+ config.active_storage.previewers = [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
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.variable_content_types = %w(
+ image/png
+ image/gif
+ image/jpg
+ image/jpeg
+ image/vnd.adobe.photoshop
+ image/vnd.microsoft.icon
+ )
+
config.active_storage.content_types_to_serve_as_binary = %w(
text/html
text/javascript
@@ -60,7 +69,7 @@ module ActiveStorage
end
initializer "active_storage.services" do
- config.to_prepare do
+ ActiveSupport.on_load(:active_storage_blob) do
if config_choice = Rails.configuration.active_storage.service
configs = Rails.configuration.active_storage.service_configurations ||= begin
config_file = Pathname.new(Rails.root.join("config/storage.yml"))
diff --git a/activestorage/lib/active_storage/previewer/mupdf_previewer.rb b/activestorage/lib/active_storage/previewer/mupdf_previewer.rb
new file mode 100644
index 0000000000..ae02a4889d
--- /dev/null
+++ b/activestorage/lib/active_storage/previewer/mupdf_previewer.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Previewer::MuPDFPreviewer < Previewer
+ class << self
+ def accept?(blob)
+ blob.content_type == "application/pdf" && mutool_exists?
+ end
+
+ def mutool_path
+ ActiveStorage.paths[:mutool] || "mutool"
+ end
+
+ def mutool_exists?
+ return @mutool_exists unless @mutool_exists.nil?
+
+ system mutool_path, out: File::NULL, err: File::NULL
+
+ @mutool_exists = $?.exitstatus == 1
+ end
+ end
+
+ def preview
+ download_blob_to_tempfile do |input|
+ draw_first_page_from input do |output|
+ yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
+ end
+ end
+ end
+
+ private
+ def draw_first_page_from(file, &block)
+ draw self.class.mutool_path, "draw", "-F", "png", "-o", "-", file.path, "1", &block
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/previewer/pdf_previewer.rb b/activestorage/lib/active_storage/previewer/pdf_previewer.rb
deleted file mode 100644
index 426ff51eb1..0000000000
--- a/activestorage/lib/active_storage/previewer/pdf_previewer.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveStorage
- class Previewer::PDFPreviewer < Previewer
- def self.accept?(blob)
- blob.content_type == "application/pdf"
- end
-
- def preview
- download_blob_to_tempfile do |input|
- draw_first_page_from input do |output|
- yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
- end
- end
- end
-
- private
- def draw_first_page_from(file, &block)
- draw mutool_path, "draw", "-F", "png", "-o", "-", file.path, "1", &block
- end
-
- def mutool_path
- ActiveStorage.paths[:mutool] || "mutool"
- end
- end
-end
diff --git a/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb b/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb
new file mode 100644
index 0000000000..2a787362cf
--- /dev/null
+++ b/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Previewer::PopplerPDFPreviewer < Previewer
+ class << self
+ def accept?(blob)
+ blob.content_type == "application/pdf" && pdftoppm_exists?
+ end
+
+ def pdftoppm_path
+ ActiveStorage.paths[:pdftoppm] || "pdftoppm"
+ end
+
+ def pdftoppm_exists?
+ return @pdftoppm_exists unless @pdftoppm_exists.nil?
+
+ @pdftoppm_exists = system(pdftoppm_path, "-v", out: File::NULL, err: File::NULL)
+ end
+ end
+
+ def preview
+ download_blob_to_tempfile do |input|
+ draw_first_page_from input do |output|
+ yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
+ end
+ end
+ end
+
+ private
+ def draw_first_page_from(file, &block)
+ # use 72 dpi to match thumbnail dimesions of the PDF
+ draw self.class.pdftoppm_path, "-singlefile", "-r", "72", "-png", file.path, &block
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/service.rb b/activestorage/lib/active_storage/service.rb
index f2e1269f27..949969fc95 100644
--- a/activestorage/lib/active_storage/service.rb
+++ b/activestorage/lib/active_storage/service.rb
@@ -73,6 +73,11 @@ module ActiveStorage
raise NotImplementedError
end
+ # Return the partial content in the byte +range+ of the file at the +key+.
+ def download_chunk(key, range)
+ raise NotImplementedError
+ end
+
# Delete the file at the +key+.
def delete(key)
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 0a9eb7f23d..2867a4e441 100644
--- a/activestorage/lib/active_storage/service/azure_storage_service.rb
+++ b/activestorage/lib/active_storage/service/azure_storage_service.rb
@@ -8,14 +8,13 @@ module ActiveStorage
# Wraps the Microsoft Azure Storage Blob Service as an Active Storage service.
# See ActiveStorage::Service for the generic API documentation that applies to all services.
class Service::AzureStorageService < Service
- attr_reader :client, :path, :blobs, :container, :signer
+ attr_reader :client, :blobs, :container, :signer
- def initialize(path:, storage_account_name:, storage_access_key:, container:)
+ def initialize(storage_account_name:, storage_access_key:, container:)
@client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key)
@signer = Azure::Storage::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
@blobs = client.blob_client
@container = container
- @path = path
end
def upload(key, io, checksum: nil)
@@ -41,6 +40,13 @@ module ActiveStorage
end
end
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ _, io = blobs.get_blob(container, key, start_range: range.begin, end_range: range.exclude_end? ? range.end - 1 : range.end)
+ io.force_encoding(Encoding::BINARY)
+ end
+ end
+
def delete(key)
instrument :delete, key: key do
begin
@@ -77,9 +83,9 @@ module ActiveStorage
def url(key, expires_in:, filename:, disposition:, content_type:)
instrument :url, key: key do |payload|
- base_url = url_for(key)
generated_url = signer.signed_uri(
- URI(base_url), false,
+ uri_for(key), false,
+ service: "b",
permissions: "r",
expiry: format_expiry(expires_in),
content_disposition: content_disposition_with(type: disposition, filename: filename),
@@ -94,9 +100,12 @@ module ActiveStorage
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
instrument :url, key: key do |payload|
- base_url = url_for(key)
- generated_url = signer.signed_uri(URI(base_url), false, permissions: "rw",
- expiry: format_expiry(expires_in)).to_s
+ generated_url = signer.signed_uri(
+ uri_for(key), false,
+ service: "b",
+ permissions: "rw",
+ expiry: format_expiry(expires_in)
+ ).to_s
payload[:url] = generated_url
@@ -109,8 +118,8 @@ module ActiveStorage
end
private
- def url_for(key)
- "#{path}/#{container}/#{key}"
+ def uri_for(key)
+ blobs.generate_uri("#{container}/#{key}")
end
def blob_for(key)
diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb
index d17eea9046..5b652fe74e 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, :host
+ attr_reader :root
- def initialize(root:, host: "http://localhost:3000")
- @root, @host = root, host
+ def initialize(root:)
+ @root = root
end
def upload(key, io, checksum: nil)
@@ -38,6 +38,15 @@ module ActiveStorage
end
end
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ File.open(path_for(key), "rb") do |file|
+ file.seek range.begin
+ file.read range.size
+ end
+ end
+ end
+
def delete(key)
instrument :delete, key: key do
begin
@@ -69,12 +78,12 @@ module ActiveStorage
verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key)
generated_url =
- Rails.application.routes.url_helpers.rails_disk_service_url(
+ url_helpers.rails_disk_service_url(
verified_key_with_expiration,
+ host: current_host,
filename: filename,
disposition: content_disposition_with(type: disposition, filename: filename),
- content_type: content_type,
- host: host
+ content_type: content_type
)
payload[:url] = generated_url
@@ -96,7 +105,7 @@ module ActiveStorage
purpose: :blob_token }
)
- generated_url = Rails.application.routes.url_helpers.update_rails_disk_service_url(verified_token_with_expiration, host: host)
+ generated_url = url_helpers.update_rails_disk_service_url(verified_token_with_expiration, host: current_host)
payload[:url] = generated_url
@@ -127,5 +136,13 @@ module ActiveStorage
raise ActiveStorage::IntegrityError
end
end
+
+ def url_helpers
+ @url_helpers ||= Rails.application.routes.url_helpers
+ end
+
+ def current_host
+ ActiveStorage::Current.host
+ end
end
end
diff --git a/activestorage/lib/active_storage/service/gcs_service.rb b/activestorage/lib/active_storage/service/gcs_service.rb
index 6f6f4105fe..369c33cbdb 100644
--- a/activestorage/lib/active_storage/service/gcs_service.rb
+++ b/activestorage/lib/active_storage/service/gcs_service.rb
@@ -3,7 +3,10 @@
gem "google-cloud-storage", "~> 1.8"
require "google/cloud/storage"
+require "net/http"
+
require "active_support/core_ext/object/to_query"
+require "active_storage/filename"
module ActiveStorage
# Wraps the Google Cloud Storage as an Active Storage service. See ActiveStorage::Service for the generic API
@@ -43,6 +46,16 @@ module ActiveStorage
end
end
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ uri = URI(url(key, expires_in: 30.seconds, filename: ActiveStorage::Filename.new(""), content_type: "application/octet-stream", disposition: "inline"))
+
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |client|
+ client.get(uri, "Range" => "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body
+ end
+ end
+ end
+
def delete(key)
instrument :delete, key: key do
begin
@@ -80,10 +93,9 @@ module ActiveStorage
end
end
- def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
+ def url_for_direct_upload(key, expires_in:, checksum:, **)
instrument :url, key: key do |payload|
- generated_url = bucket.signed_url key, method: "PUT", expires: expires_in,
- content_type: content_type, content_md5: checksum
+ generated_url = bucket.signed_url key, method: "PUT", expires: expires_in, content_md5: checksum
payload[:url] = generated_url
@@ -91,8 +103,8 @@ module ActiveStorage
end
end
- def headers_for_direct_upload(key, content_type:, checksum:, **)
- { "Content-Type" => content_type, "Content-MD5" => checksum }
+ def headers_for_direct_upload(key, checksum:, **)
+ { "Content-MD5" => checksum }
end
private
diff --git a/activestorage/lib/active_storage/service/mirror_service.rb b/activestorage/lib/active_storage/service/mirror_service.rb
index 7eca8ce7f4..6002ef5a00 100644
--- a/activestorage/lib/active_storage/service/mirror_service.rb
+++ b/activestorage/lib/active_storage/service/mirror_service.rb
@@ -9,7 +9,7 @@ module ActiveStorage
class Service::MirrorService < Service
attr_reader :primary, :mirrors
- delegate :download, :exist?, :url, to: :primary
+ delegate :download, :download_chunk, :exist?, :url, to: :primary
# Stitch together from named services.
def self.build(primary:, mirrors:, configurator:, **options) #:nodoc:
diff --git a/activestorage/lib/active_storage/service/s3_service.rb b/activestorage/lib/active_storage/service/s3_service.rb
index c95672f338..5e489f4be1 100644
--- a/activestorage/lib/active_storage/service/s3_service.rb
+++ b/activestorage/lib/active_storage/service/s3_service.rb
@@ -9,8 +9,8 @@ module ActiveStorage
class Service::S3Service < Service
attr_reader :client, :bucket, :upload_options
- def initialize(access_key_id:, secret_access_key:, region:, bucket:, upload: {}, **options)
- @client = Aws::S3::Resource.new(access_key_id: access_key_id, secret_access_key: secret_access_key, region: region, **options)
+ def initialize(bucket:, upload: {}, **options)
+ @client = Aws::S3::Resource.new(**options)
@bucket = @client.bucket(bucket)
@upload_options = upload
@@ -38,6 +38,12 @@ module ActiveStorage
end
end
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ object_for(key).get(range: "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body.read.force_encoding(Encoding::BINARY)
+ end
+ end
+
def delete(key)
instrument :delete, key: key do
object_for(key).delete
diff --git a/activestorage/package.json b/activestorage/package.json
index ec77dc391d..d23f8b179e 100644
--- a/activestorage/package.json
+++ b/activestorage/package.json
@@ -4,7 +4,8 @@
"description": "Attach cloud and local files in Rails applications",
"main": "app/assets/javascripts/activestorage.js",
"files": [
- "app/assets/javascripts/*.js"
+ "app/assets/javascripts/*.js",
+ "src/*.js"
],
"homepage": "http://rubyonrails.org/",
"repository": {
@@ -16,18 +17,21 @@
},
"author": "Javan Makhmali <javan@javan.us>",
"license": "MIT",
+ "dependencies": {
+ "spark-md5": "^3.0.0"
+ },
"devDependencies": {
"babel-core": "^6.25.0",
"babel-loader": "^7.1.1",
"babel-preset-env": "^1.6.0",
"eslint": "^4.3.0",
"eslint-plugin-import": "^2.7.0",
- "spark-md5": "^3.0.0",
"webpack": "^3.4.0"
},
"scripts": {
"prebuild": "yarn lint",
"build": "webpack -p",
- "lint": "eslint app/javascript"
+ "lint": "eslint app/javascript",
+ "prepublishOnly": "rm -rf src && cp -R app/javascript/activestorage src"
}
}
diff --git a/activestorage/test/controllers/direct_uploads_controller_test.rb b/activestorage/test/controllers/direct_uploads_controller_test.rb
index 888767086c..1b16da17d9 100644
--- a/activestorage/test/controllers/direct_uploads_controller_test.rb
+++ b/activestorage/test/controllers/direct_uploads_controller_test.rb
@@ -27,7 +27,7 @@ if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].pr
assert_equal checksum, details["checksum"]
assert_equal "text/plain", details["content_type"]
assert_match SERVICE_CONFIGURATIONS[:s3][:bucket], details["direct_upload"]["url"]
- assert_match(/s3\.(\S+)?amazonaws\.com/, details["direct_upload"]["url"])
+ assert_match(/s3(-[-a-z0-9]+)?\.(\S+)?amazonaws\.com/, details["direct_upload"]["url"])
assert_equal({ "Content-Type" => "text/plain", "Content-MD5" => checksum }, details["direct_upload"]["headers"])
end
end
@@ -62,7 +62,7 @@ if SERVICE_CONFIGURATIONS[:gcs]
assert_equal checksum, details["checksum"]
assert_equal "text/plain", details["content_type"]
assert_match %r{storage\.googleapis\.com/#{@config[:bucket]}}, details["direct_upload"]["url"]
- assert_equal({ "Content-Type" => "text/plain", "Content-MD5" => checksum }, details["direct_upload"]["headers"])
+ assert_equal({ "Content-MD5" => checksum }, details["direct_upload"]["headers"])
end
end
end
@@ -121,4 +121,27 @@ class ActiveStorage::DiskDirectUploadsControllerTest < ActionDispatch::Integrati
assert_equal({ "Content-Type" => "text/plain" }, details["direct_upload"]["headers"])
end
end
+
+ test "creating new direct upload does not include root in json" do
+ checksum = Digest::MD5.base64digest("Hello")
+
+ set_include_root_in_json(true) do
+ post rails_direct_uploads_url, params: { blob: {
+ filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain" } }
+ end
+
+ @response.parsed_body.tap do |details|
+ assert_nil details["blob"]
+ assert_not_nil details["id"]
+ end
+ end
+
+ private
+ def set_include_root_in_json(value)
+ original = ActiveRecord::Base.include_root_in_json
+ ActiveRecord::Base.include_root_in_json = value
+ yield
+ ensure
+ ActiveRecord::Base.include_root_in_json = original
+ end
end
diff --git a/activestorage/test/controllers/previews_controller_test.rb b/activestorage/test/controllers/previews_controller_test.rb
deleted file mode 100644
index b87be6c8b2..0000000000
--- a/activestorage/test/controllers/previews_controller_test.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-require "database/setup"
-
-class ActiveStorage::PreviewsControllerTest < ActionDispatch::IntegrationTest
- setup do
- @blob = create_file_blob filename: "report.pdf", content_type: "application/pdf"
- end
-
- test "showing preview inline" do
- get rails_blob_preview_url(
- filename: @blob.filename,
- signed_blob_id: @blob.signed_id,
- variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
-
- assert_predicate @blob.preview_image, :attached?
- assert_redirected_to(/report\.png\?.*disposition=inline/)
-
- image = read_image(@blob.preview_image.variant(resize: "100x100"))
- assert_equal 77, image.width
- assert_equal 100, image.height
- end
-
- test "showing preview with invalid signed blob ID" do
- get rails_blob_preview_url(
- filename: @blob.filename,
- signed_blob_id: "invalid",
- variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
-
- assert_response :not_found
- end
-end
diff --git a/activestorage/test/controllers/representations_controller_test.rb b/activestorage/test/controllers/representations_controller_test.rb
new file mode 100644
index 0000000000..2662cc5283
--- /dev/null
+++ b/activestorage/test/controllers/representations_controller_test.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::RepresentationsControllerWithVariantsTest < ActionDispatch::IntegrationTest
+ setup do
+ @blob = create_file_blob filename: "racecar.jpg"
+ end
+
+ test "showing variant inline" do
+ get rails_blob_representation_url(
+ filename: @blob.filename,
+ signed_blob_id: @blob.signed_id,
+ variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
+
+ assert_redirected_to(/racecar\.jpg\?.*disposition=inline/)
+
+ image = read_image(@blob.variant(resize: "100x100"))
+ assert_equal 100, image.width
+ assert_equal 67, image.height
+ end
+
+ test "showing variant with invalid signed blob ID" do
+ get rails_blob_representation_url(
+ filename: @blob.filename,
+ signed_blob_id: "invalid",
+ variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
+
+ assert_response :not_found
+ end
+end
+
+class ActiveStorage::RepresentationsControllerWithPreviewsTest < ActionDispatch::IntegrationTest
+ setup do
+ @blob = create_file_blob filename: "report.pdf", content_type: "application/pdf"
+ end
+
+ test "showing preview inline" do
+ get rails_blob_representation_url(
+ filename: @blob.filename,
+ signed_blob_id: @blob.signed_id,
+ variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
+
+ assert_predicate @blob.preview_image, :attached?
+ assert_redirected_to(/report\.png\?.*disposition=inline/)
+
+ image = read_image(@blob.preview_image.variant(resize: "100x100"))
+ assert_equal 77, image.width
+ assert_equal 100, image.height
+ end
+
+ test "showing preview with invalid signed blob ID" do
+ get rails_blob_representation_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
deleted file mode 100644
index a0642f9bed..0000000000
--- a/activestorage/test/controllers/variants_controller_test.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-require "database/setup"
-
-class ActiveStorage::VariantsControllerTest < ActionDispatch::IntegrationTest
- setup do
- @blob = create_file_blob filename: "racecar.jpg"
- end
-
- test "showing variant inline" do
- get rails_blob_variation_url(
- filename: @blob.filename,
- signed_blob_id: @blob.signed_id,
- variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
-
- assert_redirected_to(/racecar\.jpg\?.*disposition=inline/)
-
- image = read_image(@blob.variant(resize: "100x100"))
- assert_equal 100, image.width
- assert_equal 67, image.height
- end
-
- test "showing variant with invalid signed blob ID" do
- get rails_blob_variation_url(
- filename: @blob.filename,
- signed_blob_id: "invalid",
- variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
-
- assert_response :not_found
- end
-end
diff --git a/activestorage/test/fixtures/files/favicon.ico b/activestorage/test/fixtures/files/favicon.ico
new file mode 100644
index 0000000000..87192a8a07
--- /dev/null
+++ b/activestorage/test/fixtures/files/favicon.ico
Binary files differ
diff --git a/activestorage/test/models/attachments_test.rb b/activestorage/test/models/attachments_test.rb
index 9ba2759893..ce83ec27d2 100644
--- a/activestorage/test/models/attachments_test.rb
+++ b/activestorage/test/models/attachments_test.rb
@@ -56,6 +56,44 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
assert ActiveStorage::Blob.service.exist?(@user.avatar.key)
end
+ test "replace attached blob with itself" do
+ @user.avatar.attach create_blob(filename: "funky.jpg")
+
+ assert_no_changes -> { @user.reload.avatar.blob } do
+ assert_no_changes -> { @user.reload.avatar.attachment } do
+ assert_no_enqueued_jobs do
+ @user.avatar.attach @user.avatar.blob
+ end
+ end
+ end
+ end
+
+ test "replaced attached blob with itself by signed ID" do
+ @user.avatar.attach create_blob(filename: "funky.jpg")
+
+ assert_no_changes -> { @user.reload.avatar.blob } do
+ assert_no_changes -> { @user.reload.avatar.attachment } do
+ assert_no_enqueued_jobs do
+ @user.avatar.attach @user.avatar.blob.signed_id
+ end
+ end
+ end
+ end
+
+ test "replace independent attached blob" do
+ @user.cover_photo.attach create_blob(filename: "funky.jpg")
+
+ perform_enqueued_jobs do
+ assert_difference -> { ActiveStorage::Blob.count }, +1 do
+ assert_no_difference -> { ActiveStorage::Attachment.count } do
+ @user.cover_photo.attach create_blob(filename: "town.jpg")
+ end
+ end
+ end
+
+ assert_equal "town.jpg", @user.cover_photo.filename.to_s
+ end
+
test "attach blob to new record" do
user = User.new(name: "Jason")
@@ -98,17 +136,26 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
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)
+ blob = directly_upload_file_blob(content_type: "application/octet-stream")
- 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_predicate @user.avatar, :identified?
end
+ test "identify and analyze newly-attached, directly-uploaded blob" do
+ blob = directly_upload_file_blob(content_type: "application/octet-stream")
+
+ perform_enqueued_jobs do
+ @user.avatar.attach blob
+ end
+
+ assert_equal true, @user.avatar.reload.metadata[:identified]
+ assert_equal 4104, @user.avatar.metadata[:width]
+ assert_equal 2736, @user.avatar.metadata[:height]
+ end
+
test "identify newly-attached blob only once" do
blob = create_file_blob
assert_predicate blob, :identified?
@@ -180,13 +227,20 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
avatar_key = @user.avatar.key
perform_enqueued_jobs do
- @user.destroy
+ @user.reload.destroy
assert_nil ActiveStorage::Blob.find_by(key: avatar_key)
assert_not ActiveStorage::Blob.service.exist?(avatar_key)
end
end
+ test "delete attachment for independent blob when record is destroyed" do
+ @user.cover_photo.attach create_blob(filename: "funky.jpg")
+
+ @user.destroy
+ assert_not ActiveStorage::Attachment.exists?(record: @user, name: "cover_photo")
+ end
+
test "find with attached blob" do
records = %w[alice bob].map do |name|
User.create!(name: name).tap do |user|
@@ -358,7 +412,7 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
highlight_keys = @user.highlights.collect(&:key)
perform_enqueued_jobs do
- @user.destroy
+ @user.reload.destroy
assert_nil ActiveStorage::Blob.find_by(key: highlight_keys.first)
assert_not ActiveStorage::Blob.service.exist?(highlight_keys.first)
@@ -367,4 +421,39 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase
assert_not ActiveStorage::Blob.service.exist?(highlight_keys.second)
end
end
+
+ test "delete attachments for independent blobs when the record is destroyed" do
+ @user.vlogs.attach create_blob(filename: "funky.mp4"), create_blob(filename: "wonky.mp4")
+
+ @user.destroy
+ assert_not ActiveStorage::Attachment.exists?(record: @user, name: "vlogs")
+ end
+
+ test "selectively purge one attached blob of many" do
+ first_blob = create_blob(filename: "funky.jpg")
+ second_blob = create_blob(filename: "wonky.jpg")
+ attachments = @user.highlights.attach(first_blob, second_blob)
+
+ assert_difference -> { ActiveStorage::Blob.count }, -1 do
+ @user.highlights.where(id: attachments.first.id).purge
+ end
+
+ assert_not ActiveStorage::Blob.exists?(key: first_blob.key)
+ assert ActiveStorage::Blob.exists?(key: second_blob.key)
+ end
+
+ test "selectively purge one attached blob of many later" do
+ first_blob = create_blob(filename: "funky.jpg")
+ second_blob = create_blob(filename: "wonky.jpg")
+ attachments = @user.highlights.attach(first_blob, second_blob)
+
+ perform_enqueued_jobs do
+ assert_difference -> { ActiveStorage::Blob.count }, -1 do
+ @user.highlights.where(id: attachments.first.id).purge_later
+ end
+ end
+
+ assert_not ActiveStorage::Blob.exists?(key: first_blob.key)
+ assert ActiveStorage::Blob.exists?(key: second_blob.key)
+ end
end
diff --git a/activestorage/test/models/blob_test.rb b/activestorage/test/models/blob_test.rb
index 5cd2a94326..fead17d33a 100644
--- a/activestorage/test/models/blob_test.rb
+++ b/activestorage/test/models/blob_test.rb
@@ -7,6 +7,23 @@ require "active_support/testing/method_call_assertions"
class ActiveStorage::BlobTest < ActiveSupport::TestCase
include ActiveSupport::Testing::MethodCallAssertions
+ test ".unattached scope returns not attached blobs" do
+ class UserWithHasOneAttachedDependentFalse < User
+ has_one_attached :avatar, dependent: false
+ end
+
+ ActiveStorage::Blob.delete_all
+ blob_1 = create_blob filename: "funky.jpg"
+ blob_2 = create_blob filename: "town.jpg"
+
+ user = UserWithHasOneAttachedDependentFalse.create!
+ user.avatar.attach blob_1
+
+ assert_equal [blob_2], ActiveStorage::Blob.unattached
+ user.destroy
+ assert_equal [blob_1, blob_2].map(&:id).sort, ActiveStorage::Blob.unattached.pluck(:id).sort
+ end
+
test "create after upload sets byte size and checksum" do
data = "Hello world!"
blob = create_blob data: data
@@ -82,6 +99,8 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase
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)
+ assert_equal expected_url_for(blob, filename: new_filename), blob.service_url(filename: "new.txt")
+ assert_equal expected_url_for(blob, filename: blob.filename), blob.service_url(filename: nil)
end
end
@@ -121,6 +140,6 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase
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}"
+ "https://example.com/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/variant_test.rb b/activestorage/test/models/variant_test.rb
index 0cf8a583bd..0f3ada25c0 100644
--- a/activestorage/test/models/variant_test.rb
+++ b/activestorage/test/models/variant_test.rb
@@ -50,6 +50,17 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase
assert_equal 20, image.height
end
+ test "resized variation of ICO blob" do
+ blob = create_file_blob(filename: "favicon.ico", content_type: "image/vnd.microsoft.icon")
+ 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")
diff --git a/activestorage/test/previewer/pdf_previewer_test.rb b/activestorage/test/previewer/mupdf_previewer_test.rb
index fe32f39be4..6c2db6fcbf 100644
--- a/activestorage/test/previewer/pdf_previewer_test.rb
+++ b/activestorage/test/previewer/mupdf_previewer_test.rb
@@ -3,15 +3,15 @@
require "test_helper"
require "database/setup"
-require "active_storage/previewer/pdf_previewer"
+require "active_storage/previewer/mupdf_previewer"
-class ActiveStorage::Previewer::PDFPreviewerTest < ActiveSupport::TestCase
+class ActiveStorage::Previewer::MuPDFPreviewerTest < ActiveSupport::TestCase
setup do
@blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf")
end
test "previewing a PDF document" do
- ActiveStorage::Previewer::PDFPreviewer.new(@blob).preview do |attachable|
+ ActiveStorage::Previewer::MuPDFPreviewer.new(@blob).preview do |attachable|
assert_equal "image/png", attachable[:content_type]
assert_equal "report.png", attachable[:filename]
diff --git a/activestorage/test/previewer/poppler_pdf_previewer_test.rb b/activestorage/test/previewer/poppler_pdf_previewer_test.rb
new file mode 100644
index 0000000000..2b41c8b642
--- /dev/null
+++ b/activestorage/test/previewer/poppler_pdf_previewer_test.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+require "active_storage/previewer/poppler_pdf_previewer"
+
+class ActiveStorage::Previewer::PopplerPDFPreviewerTest < ActiveSupport::TestCase
+ setup do
+ @blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf")
+ end
+
+ test "previewing a PDF document" do
+ ActiveStorage::Previewer::PopplerPDFPreviewer.new(@blob).preview do |attachable|
+ assert_equal "image/png", attachable[:content_type]
+ assert_equal "report.png", attachable[:filename]
+
+ image = MiniMagick::Image.read(attachable[:io])
+ assert_equal 612, image.width
+ assert_equal 792, image.height
+ end
+ end
+end
diff --git a/activestorage/test/service/configurations.example.yml b/activestorage/test/service/configurations.example.yml
index 43cc013bc8..a63aa33302 100644
--- a/activestorage/test/service/configurations.example.yml
+++ b/activestorage/test/service/configurations.example.yml
@@ -24,7 +24,6 @@
#
# azure:
# service: AzureStorage
-# path: ""
# storage_account_name: ""
# storage_access_key: ""
# container: ""
diff --git a/activestorage/test/service/configurations.yml.enc b/activestorage/test/service/configurations.yml.enc
index df11aac161..648924a562 100644
--- a/activestorage/test/service/configurations.yml.enc
+++ b/activestorage/test/service/configurations.yml.enc
Binary files differ
diff --git a/activestorage/test/service/configurator_test.rb b/activestorage/test/service/configurator_test.rb
index fe8a637ad0..1c9c5c3aa0 100644
--- a/activestorage/test/service/configurator_test.rb
+++ b/activestorage/test/service/configurator_test.rb
@@ -5,10 +5,8 @@ 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/disk_service_test.rb b/activestorage/test/service/disk_service_test.rb
index 4a6361b920..d7142de458 100644
--- a/activestorage/test/service/disk_service_test.rb
+++ b/activestorage/test/service/disk_service_test.rb
@@ -8,7 +8,7 @@ class ActiveStorage::Service::DiskServiceTest < ActiveSupport::TestCase
include ActiveStorage::Service::SharedServiceTests
test "url generation" do
- assert_match(/rails\/active_storage\/disk\/.*\/avatar\.png\?content_type=image%2Fpng&disposition=inline/,
+ assert_match(/^https:\/\/example.com\/rails\/active_storage\/disk\/.*\/avatar\.png\?content_type=image%2Fpng&disposition=inline/,
@service.url(FIXTURE_KEY, expires_in: 5.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png"))
end
end
diff --git a/activestorage/test/service/gcs_service_test.rb b/activestorage/test/service/gcs_service_test.rb
index 7efcd60fb7..fc2d9d0fa7 100644
--- a/activestorage/test/service/gcs_service_test.rb
+++ b/activestorage/test/service/gcs_service_test.rb
@@ -19,7 +19,7 @@ if SERVICE_CONFIGURATIONS[:gcs]
uri = URI.parse url
request = Net::HTTP::Put.new uri.request_uri
request.body = data
- request.add_field "Content-Type", "text/plain"
+ request.add_field "Content-Type", ""
request.add_field "Content-MD5", checksum
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request request
diff --git a/activestorage/test/service/s3_service_test.rb b/activestorage/test/service/s3_service_test.rb
index c3818422aa..7833e51122 100644
--- a/activestorage/test/service/s3_service_test.rb
+++ b/activestorage/test/service/s3_service_test.rb
@@ -3,7 +3,7 @@
require "service/shared_service_tests"
require "net/http"
-if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].present?
+if SERVICE_CONFIGURATIONS[:s3]
class ActiveStorage::Service::S3ServiceTest < ActiveSupport::TestCase
SERVICE = ActiveStorage::Service.configure(:s3, SERVICE_CONFIGURATIONS)
@@ -35,7 +35,7 @@ if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].pr
url = @service.url(FIXTURE_KEY, expires_in: 5.minutes,
disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png")
- assert_match(/s3\.(\S+)?amazonaws.com.*response-content-disposition=inline.*avatar\.png.*response-content-type=image%2Fpng/, url)
+ assert_match(/s3(-[-a-z0-9]+)?\.(\S+)?amazonaws.com.*response-content-disposition=inline.*avatar\.png.*response-content-type=image%2Fpng/, url)
assert_match SERVICE_CONFIGURATIONS[:s3][:bucket], url
end
diff --git a/activestorage/test/service/shared_service_tests.rb b/activestorage/test/service/shared_service_tests.rb
index ce28c4393a..24debe7f47 100644
--- a/activestorage/test/service/shared_service_tests.rb
+++ b/activestorage/test/service/shared_service_tests.rb
@@ -60,6 +60,11 @@ module ActiveStorage::Service::SharedServiceTests
assert_equal [ FIXTURE_DATA ], chunks
end
+ test "downloading partially" do
+ assert_equal "\x10\x00\x00", @service.download_chunk(FIXTURE_KEY, 19..21)
+ assert_equal "\x10\x00\x00", @service.download_chunk(FIXTURE_KEY, 19...22)
+ end
+
test "existing" do
assert @service.exist?(FIXTURE_KEY)
assert_not @service.exist?(FIXTURE_KEY + "nonsense")
diff --git a/activestorage/test/test_helper.rb b/activestorage/test/test_helper.rb
index 98fa44a604..028874f374 100644
--- a/activestorage/test/test_helper.rb
+++ b/activestorage/test/test_helper.rb
@@ -7,7 +7,6 @@ require "bundler/setup"
require "active_support"
require "active_support/test_case"
require "active_support/testing/autorun"
-require "webmock/minitest"
require "mini_magick"
begin
@@ -42,7 +41,13 @@ ActiveStorage.verifier = ActiveSupport::MessageVerifier.new("Testing")
class ActiveSupport::TestCase
self.file_fixture_path = File.expand_path("fixtures/files", __dir__)
- setup { WebMock.allow_net_connect! }
+ setup do
+ ActiveStorage::Current.host = "https://example.com"
+ end
+
+ teardown do
+ ActiveStorage::Current.reset
+ end
private
def create_blob(data: "Hello world!", filename: "hello.txt", content_type: "text/plain")
@@ -57,6 +62,16 @@ class ActiveSupport::TestCase
ActiveStorage::Blob.create_before_direct_upload! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type
end
+ def directly_upload_file_blob(filename: "racecar.jpg", content_type: "image/jpeg")
+ file = file_fixture(filename)
+ byte_size = file.size
+ checksum = Digest::MD5.file(file).base64digest
+
+ create_blob_before_direct_upload(filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type).tap do |blob|
+ ActiveStorage::Blob.service.upload(blob.key, file.open)
+ end
+ end
+
def read_image(blob_or_variant)
MiniMagick::Image.open blob_or_variant.service.send(:path_for, blob_or_variant.key)
end
@@ -68,5 +83,8 @@ ActiveRecord::Base.send :include, GlobalID::Identification
class User < ActiveRecord::Base
has_one_attached :avatar
+ has_one_attached :cover_photo, dependent: false
+
has_many_attached :highlights
+ has_many_attached :vlogs, dependent: false
end