activestorage 6.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activestorage might be problematic. Click here for more details.

Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +198 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +162 -0
  5. data/app/assets/javascripts/activestorage.js +942 -0
  6. data/app/controllers/active_storage/base_controller.rb +8 -0
  7. data/app/controllers/active_storage/blobs_controller.rb +14 -0
  8. data/app/controllers/active_storage/direct_uploads_controller.rb +23 -0
  9. data/app/controllers/active_storage/disk_controller.rb +66 -0
  10. data/app/controllers/active_storage/representations_controller.rb +14 -0
  11. data/app/controllers/concerns/active_storage/set_blob.rb +16 -0
  12. data/app/controllers/concerns/active_storage/set_current.rb +15 -0
  13. data/app/javascript/activestorage/blob_record.js +73 -0
  14. data/app/javascript/activestorage/blob_upload.js +35 -0
  15. data/app/javascript/activestorage/direct_upload.js +48 -0
  16. data/app/javascript/activestorage/direct_upload_controller.js +67 -0
  17. data/app/javascript/activestorage/direct_uploads_controller.js +50 -0
  18. data/app/javascript/activestorage/file_checksum.js +53 -0
  19. data/app/javascript/activestorage/helpers.js +51 -0
  20. data/app/javascript/activestorage/index.js +11 -0
  21. data/app/javascript/activestorage/ujs.js +86 -0
  22. data/app/jobs/active_storage/analyze_job.rb +12 -0
  23. data/app/jobs/active_storage/base_job.rb +4 -0
  24. data/app/jobs/active_storage/purge_job.rb +13 -0
  25. data/app/models/active_storage/attachment.rb +50 -0
  26. data/app/models/active_storage/blob.rb +278 -0
  27. data/app/models/active_storage/blob/analyzable.rb +57 -0
  28. data/app/models/active_storage/blob/identifiable.rb +31 -0
  29. data/app/models/active_storage/blob/representable.rb +93 -0
  30. data/app/models/active_storage/current.rb +5 -0
  31. data/app/models/active_storage/filename.rb +77 -0
  32. data/app/models/active_storage/preview.rb +89 -0
  33. data/app/models/active_storage/variant.rb +131 -0
  34. data/app/models/active_storage/variation.rb +80 -0
  35. data/config/routes.rb +32 -0
  36. data/db/migrate/20170806125915_create_active_storage_tables.rb +26 -0
  37. data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +9 -0
  38. data/lib/active_storage.rb +73 -0
  39. data/lib/active_storage/analyzer.rb +38 -0
  40. data/lib/active_storage/analyzer/image_analyzer.rb +52 -0
  41. data/lib/active_storage/analyzer/null_analyzer.rb +13 -0
  42. data/lib/active_storage/analyzer/video_analyzer.rb +118 -0
  43. data/lib/active_storage/attached.rb +25 -0
  44. data/lib/active_storage/attached/changes.rb +16 -0
  45. data/lib/active_storage/attached/changes/create_many.rb +46 -0
  46. data/lib/active_storage/attached/changes/create_one.rb +69 -0
  47. data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
  48. data/lib/active_storage/attached/changes/delete_many.rb +27 -0
  49. data/lib/active_storage/attached/changes/delete_one.rb +19 -0
  50. data/lib/active_storage/attached/many.rb +65 -0
  51. data/lib/active_storage/attached/model.rb +147 -0
  52. data/lib/active_storage/attached/one.rb +79 -0
  53. data/lib/active_storage/downloader.rb +43 -0
  54. data/lib/active_storage/downloading.rb +47 -0
  55. data/lib/active_storage/engine.rb +149 -0
  56. data/lib/active_storage/errors.rb +26 -0
  57. data/lib/active_storage/gem_version.rb +17 -0
  58. data/lib/active_storage/log_subscriber.rb +58 -0
  59. data/lib/active_storage/previewer.rb +84 -0
  60. data/lib/active_storage/previewer/mupdf_previewer.rb +36 -0
  61. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +35 -0
  62. data/lib/active_storage/previewer/video_previewer.rb +26 -0
  63. data/lib/active_storage/reflection.rb +64 -0
  64. data/lib/active_storage/service.rb +141 -0
  65. data/lib/active_storage/service/azure_storage_service.rb +165 -0
  66. data/lib/active_storage/service/configurator.rb +34 -0
  67. data/lib/active_storage/service/disk_service.rb +166 -0
  68. data/lib/active_storage/service/gcs_service.rb +141 -0
  69. data/lib/active_storage/service/mirror_service.rb +55 -0
  70. data/lib/active_storage/service/s3_service.rb +116 -0
  71. data/lib/active_storage/transformers/image_processing_transformer.rb +39 -0
  72. data/lib/active_storage/transformers/mini_magick_transformer.rb +38 -0
  73. data/lib/active_storage/transformers/transformer.rb +42 -0
  74. data/lib/active_storage/version.rb +10 -0
  75. data/lib/tasks/activestorage.rake +22 -0
  76. metadata +174 -0
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The base class for all Active Storage controllers.
4
+ class ActiveStorage::BaseController < ActionController::Base
5
+ include ActiveStorage::SetCurrent
6
+
7
+ protect_from_forgery with: :exception
8
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Take a signed permanent reference for a blob and turn it into an expiring service URL for download.
4
+ # Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
5
+ # security-through-obscurity factor of the signed blob references, you'll need to implement your own
6
+ # authenticated redirection controller.
7
+ class ActiveStorage::BlobsController < ActiveStorage::BaseController
8
+ include ActiveStorage::SetBlob
9
+
10
+ def show
11
+ expires_in ActiveStorage.service_urls_expire_in
12
+ redirect_to @blob.service_url(disposition: params[:disposition])
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Creates a new blob on the server side in anticipation of a direct-to-service upload from the client side.
4
+ # When the client-side upload is completed, the signed_blob_id can be submitted as part of the form to reference
5
+ # the blob that was created up front.
6
+ class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
7
+ def create
8
+ blob = ActiveStorage::Blob.create_before_direct_upload!(blob_args)
9
+ render json: direct_upload_json(blob)
10
+ end
11
+
12
+ private
13
+ def blob_args
14
+ params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, :metadata).to_h.symbolize_keys
15
+ end
16
+
17
+ def direct_upload_json(blob)
18
+ blob.as_json(root: false, methods: :signed_id).merge(direct_upload: {
19
+ url: blob.service_url_for_direct_upload,
20
+ headers: blob.service_headers_for_direct_upload
21
+ })
22
+ end
23
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Serves files stored with the disk service in the same way that the cloud services do.
4
+ # This means using expiring, signed URLs that are meant for immediate access, not permanent linking.
5
+ # Always go through the BlobsController, or your own authenticated controller, rather than directly
6
+ # to the service URL.
7
+ class ActiveStorage::DiskController < ActiveStorage::BaseController
8
+ skip_forgery_protection
9
+
10
+ def show
11
+ if key = decode_verified_key
12
+ serve_file disk_service.path_for(key[:key]), content_type: key[:content_type], disposition: key[:disposition]
13
+ else
14
+ head :not_found
15
+ end
16
+ rescue Errno::ENOENT
17
+ head :not_found
18
+ end
19
+
20
+ def update
21
+ if token = decode_verified_token
22
+ if acceptable_content?(token)
23
+ disk_service.upload token[:key], request.body, checksum: token[:checksum]
24
+ else
25
+ head :unprocessable_entity
26
+ end
27
+ else
28
+ head :not_found
29
+ end
30
+ rescue ActiveStorage::IntegrityError
31
+ head :unprocessable_entity
32
+ end
33
+
34
+ private
35
+ def disk_service
36
+ ActiveStorage::Blob.service
37
+ end
38
+
39
+
40
+ def decode_verified_key
41
+ ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
42
+ end
43
+
44
+ def serve_file(path, content_type:, disposition:)
45
+ Rack::File.new(nil).serving(request, path).tap do |(status, headers, body)|
46
+ self.status = status
47
+ self.response_body = body
48
+
49
+ headers.each do |name, value|
50
+ response.headers[name] = value
51
+ end
52
+
53
+ response.headers["Content-Type"] = content_type || DEFAULT_SEND_FILE_TYPE
54
+ response.headers["Content-Disposition"] = disposition || DEFAULT_SEND_FILE_DISPOSITION
55
+ end
56
+ end
57
+
58
+
59
+ def decode_verified_token
60
+ ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
61
+ end
62
+
63
+ def acceptable_content?(token)
64
+ token[:content_type] == request.content_mime_type && token[:content_length] == request.content_length
65
+ end
66
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Take a signed permanent reference for a blob representation and turn it into an expiring service URL for download.
4
+ # Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
5
+ # security-through-obscurity factor of the signed blob and variation reference, you'll need to implement your own
6
+ # authenticated redirection controller.
7
+ class ActiveStorage::RepresentationsController < ActiveStorage::BaseController
8
+ include ActiveStorage::SetBlob
9
+
10
+ def show
11
+ expires_in ActiveStorage.service_urls_expire_in
12
+ redirect_to @blob.representation(params[:variation_key]).processed.service_url(disposition: params[:disposition])
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage::SetBlob #:nodoc:
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ before_action :set_blob
8
+ end
9
+
10
+ private
11
+ def set_blob
12
+ @blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id] || params[:signed_id])
13
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
14
+ head :not_found
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Sets the <tt>ActiveStorage::Current.host</tt> attribute, which the disk service uses to generate URLs.
4
+ # Include this concern in custom controllers that call ActiveStorage::Blob#service_url,
5
+ # ActiveStorage::Variant#service_url, or ActiveStorage::Preview#service_url so the disk service can
6
+ # generate URLs using the same host, protocol, and base path as the current request.
7
+ module ActiveStorage::SetCurrent
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ before_action do
12
+ ActiveStorage::Current.host = request.base_url
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,73 @@
1
+ import { getMetaValue } from "./helpers"
2
+
3
+ export class BlobRecord {
4
+ constructor(file, checksum, url) {
5
+ this.file = file
6
+
7
+ this.attributes = {
8
+ filename: file.name,
9
+ content_type: file.type,
10
+ byte_size: file.size,
11
+ checksum: checksum
12
+ }
13
+
14
+ this.xhr = new XMLHttpRequest
15
+ this.xhr.open("POST", url, true)
16
+ this.xhr.responseType = "json"
17
+ this.xhr.setRequestHeader("Content-Type", "application/json")
18
+ this.xhr.setRequestHeader("Accept", "application/json")
19
+ this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest")
20
+
21
+ const csrfToken = getMetaValue("csrf-token")
22
+ if (csrfToken != undefined) {
23
+ this.xhr.setRequestHeader("X-CSRF-Token", csrfToken)
24
+ }
25
+
26
+ this.xhr.addEventListener("load", event => this.requestDidLoad(event))
27
+ this.xhr.addEventListener("error", event => this.requestDidError(event))
28
+ }
29
+
30
+ get status() {
31
+ return this.xhr.status
32
+ }
33
+
34
+ get response() {
35
+ const { responseType, response } = this.xhr
36
+ if (responseType == "json") {
37
+ return response
38
+ } else {
39
+ // Shim for IE 11: https://connect.microsoft.com/IE/feedback/details/794808
40
+ return JSON.parse(response)
41
+ }
42
+ }
43
+
44
+ create(callback) {
45
+ this.callback = callback
46
+ this.xhr.send(JSON.stringify({ blob: this.attributes }))
47
+ }
48
+
49
+ requestDidLoad(event) {
50
+ if (this.status >= 200 && this.status < 300) {
51
+ const { response } = this
52
+ const { direct_upload } = response
53
+ delete response.direct_upload
54
+ this.attributes = response
55
+ this.directUploadData = direct_upload
56
+ this.callback(null, this.toJSON())
57
+ } else {
58
+ this.requestDidError(event)
59
+ }
60
+ }
61
+
62
+ requestDidError(event) {
63
+ this.callback(`Error creating Blob for "${this.file.name}". Status: ${this.status}`)
64
+ }
65
+
66
+ toJSON() {
67
+ const result = {}
68
+ for (const key in this.attributes) {
69
+ result[key] = this.attributes[key]
70
+ }
71
+ return result
72
+ }
73
+ }
@@ -0,0 +1,35 @@
1
+ export class BlobUpload {
2
+ constructor(blob) {
3
+ this.blob = blob
4
+ this.file = blob.file
5
+
6
+ const { url, headers } = blob.directUploadData
7
+
8
+ this.xhr = new XMLHttpRequest
9
+ this.xhr.open("PUT", url, true)
10
+ this.xhr.responseType = "text"
11
+ for (const key in headers) {
12
+ this.xhr.setRequestHeader(key, headers[key])
13
+ }
14
+ this.xhr.addEventListener("load", event => this.requestDidLoad(event))
15
+ this.xhr.addEventListener("error", event => this.requestDidError(event))
16
+ }
17
+
18
+ create(callback) {
19
+ this.callback = callback
20
+ this.xhr.send(this.file.slice())
21
+ }
22
+
23
+ requestDidLoad(event) {
24
+ const { status, response } = this.xhr
25
+ if (status >= 200 && status < 300) {
26
+ this.callback(null, response)
27
+ } else {
28
+ this.requestDidError(event)
29
+ }
30
+ }
31
+
32
+ requestDidError(event) {
33
+ this.callback(`Error storing "${this.file.name}". Status: ${this.xhr.status}`)
34
+ }
35
+ }
@@ -0,0 +1,48 @@
1
+ import { FileChecksum } from "./file_checksum"
2
+ import { BlobRecord } from "./blob_record"
3
+ import { BlobUpload } from "./blob_upload"
4
+
5
+ let id = 0
6
+
7
+ export class DirectUpload {
8
+ constructor(file, url, delegate) {
9
+ this.id = ++id
10
+ this.file = file
11
+ this.url = url
12
+ this.delegate = delegate
13
+ }
14
+
15
+ create(callback) {
16
+ FileChecksum.create(this.file, (error, checksum) => {
17
+ if (error) {
18
+ callback(error)
19
+ return
20
+ }
21
+
22
+ const blob = new BlobRecord(this.file, checksum, this.url)
23
+ notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr)
24
+
25
+ blob.create(error => {
26
+ if (error) {
27
+ callback(error)
28
+ } else {
29
+ const upload = new BlobUpload(blob)
30
+ notify(this.delegate, "directUploadWillStoreFileWithXHR", upload.xhr)
31
+ upload.create(error => {
32
+ if (error) {
33
+ callback(error)
34
+ } else {
35
+ callback(null, blob.toJSON())
36
+ }
37
+ })
38
+ }
39
+ })
40
+ })
41
+ }
42
+ }
43
+
44
+ function notify(object, methodName, ...messages) {
45
+ if (object && typeof object[methodName] == "function") {
46
+ return object[methodName](...messages)
47
+ }
48
+ }
@@ -0,0 +1,67 @@
1
+ import { DirectUpload } from "./direct_upload"
2
+ import { dispatchEvent } from "./helpers"
3
+
4
+ export class DirectUploadController {
5
+ constructor(input, file) {
6
+ this.input = input
7
+ this.file = file
8
+ this.directUpload = new DirectUpload(this.file, this.url, this)
9
+ this.dispatch("initialize")
10
+ }
11
+
12
+ start(callback) {
13
+ const hiddenInput = document.createElement("input")
14
+ hiddenInput.type = "hidden"
15
+ hiddenInput.name = this.input.name
16
+ this.input.insertAdjacentElement("beforebegin", hiddenInput)
17
+
18
+ this.dispatch("start")
19
+
20
+ this.directUpload.create((error, attributes) => {
21
+ if (error) {
22
+ hiddenInput.parentNode.removeChild(hiddenInput)
23
+ this.dispatchError(error)
24
+ } else {
25
+ hiddenInput.value = attributes.signed_id
26
+ }
27
+
28
+ this.dispatch("end")
29
+ callback(error)
30
+ })
31
+ }
32
+
33
+ uploadRequestDidProgress(event) {
34
+ const progress = event.loaded / event.total * 100
35
+ if (progress) {
36
+ this.dispatch("progress", { progress })
37
+ }
38
+ }
39
+
40
+ get url() {
41
+ return this.input.getAttribute("data-direct-upload-url")
42
+ }
43
+
44
+ dispatch(name, detail = {}) {
45
+ detail.file = this.file
46
+ detail.id = this.directUpload.id
47
+ return dispatchEvent(this.input, `direct-upload:${name}`, { detail })
48
+ }
49
+
50
+ dispatchError(error) {
51
+ const event = this.dispatch("error", { error })
52
+ if (!event.defaultPrevented) {
53
+ alert(error)
54
+ }
55
+ }
56
+
57
+ // DirectUpload delegate
58
+
59
+ directUploadWillCreateBlobWithXHR(xhr) {
60
+ this.dispatch("before-blob-request", { xhr })
61
+ }
62
+
63
+ directUploadWillStoreFileWithXHR(xhr) {
64
+ this.dispatch("before-storage-request", { xhr })
65
+ xhr.upload.addEventListener("progress", event => this.uploadRequestDidProgress(event))
66
+ }
67
+ }
@@ -0,0 +1,50 @@
1
+ import { DirectUploadController } from "./direct_upload_controller"
2
+ import { findElements, dispatchEvent, toArray } from "./helpers"
3
+
4
+ const inputSelector = "input[type=file][data-direct-upload-url]:not([disabled])"
5
+
6
+ export class DirectUploadsController {
7
+ constructor(form) {
8
+ this.form = form
9
+ this.inputs = findElements(form, inputSelector).filter(input => input.files.length)
10
+ }
11
+
12
+ start(callback) {
13
+ const controllers = this.createDirectUploadControllers()
14
+
15
+ const startNextController = () => {
16
+ const controller = controllers.shift()
17
+ if (controller) {
18
+ controller.start(error => {
19
+ if (error) {
20
+ callback(error)
21
+ this.dispatch("end")
22
+ } else {
23
+ startNextController()
24
+ }
25
+ })
26
+ } else {
27
+ callback()
28
+ this.dispatch("end")
29
+ }
30
+ }
31
+
32
+ this.dispatch("start")
33
+ startNextController()
34
+ }
35
+
36
+ createDirectUploadControllers() {
37
+ const controllers = []
38
+ this.inputs.forEach(input => {
39
+ toArray(input.files).forEach(file => {
40
+ const controller = new DirectUploadController(input, file)
41
+ controllers.push(controller)
42
+ })
43
+ })
44
+ return controllers
45
+ }
46
+
47
+ dispatch(name, detail = {}) {
48
+ return dispatchEvent(this.form, `direct-uploads:${name}`, { detail })
49
+ }
50
+ }
@@ -0,0 +1,53 @@
1
+ import SparkMD5 from "spark-md5"
2
+
3
+ const fileSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
4
+
5
+ export class FileChecksum {
6
+ static create(file, callback) {
7
+ const instance = new FileChecksum(file)
8
+ instance.create(callback)
9
+ }
10
+
11
+ constructor(file) {
12
+ this.file = file
13
+ this.chunkSize = 2097152 // 2MB
14
+ this.chunkCount = Math.ceil(this.file.size / this.chunkSize)
15
+ this.chunkIndex = 0
16
+ }
17
+
18
+ create(callback) {
19
+ this.callback = callback
20
+ this.md5Buffer = new SparkMD5.ArrayBuffer
21
+ this.fileReader = new FileReader
22
+ this.fileReader.addEventListener("load", event => this.fileReaderDidLoad(event))
23
+ this.fileReader.addEventListener("error", event => this.fileReaderDidError(event))
24
+ this.readNextChunk()
25
+ }
26
+
27
+ fileReaderDidLoad(event) {
28
+ this.md5Buffer.append(event.target.result)
29
+
30
+ if (!this.readNextChunk()) {
31
+ const binaryDigest = this.md5Buffer.end(true)
32
+ const base64digest = btoa(binaryDigest)
33
+ this.callback(null, base64digest)
34
+ }
35
+ }
36
+
37
+ fileReaderDidError(event) {
38
+ this.callback(`Error reading ${this.file.name}`)
39
+ }
40
+
41
+ readNextChunk() {
42
+ if (this.chunkIndex < this.chunkCount || (this.chunkIndex == 0 && this.chunkCount == 0)) {
43
+ const start = this.chunkIndex * this.chunkSize
44
+ const end = Math.min(start + this.chunkSize, this.file.size)
45
+ const bytes = fileSlice.call(this.file, start, end)
46
+ this.fileReader.readAsArrayBuffer(bytes)
47
+ this.chunkIndex++
48
+ return true
49
+ } else {
50
+ return false
51
+ }
52
+ }
53
+ }