activestorage 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

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,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A set of transformations that can be applied to a blob to create a variant. This class is exposed via
4
+ # the ActiveStorage::Blob#variant method and should rarely be used directly.
5
+ #
6
+ # In case you do need to use this directly, it's instantiated using a hash of transformations where
7
+ # the key is the command and the value is the arguments. Example:
8
+ #
9
+ # ActiveStorage::Variation.new(resize_to_limit: [100, 100], monochrome: true, trim: true, rotate: "-90")
10
+ #
11
+ # The options map directly to {ImageProcessing}[https://github.com/janko-m/image_processing] commands.
12
+ class ActiveStorage::Variation
13
+ attr_reader :transformations
14
+
15
+ class << self
16
+ # Returns a Variation instance based on the given variator. If the variator is a Variation, it is
17
+ # returned unmodified. If it is a String, it is passed to ActiveStorage::Variation.decode. Otherwise,
18
+ # it is assumed to be a transformations Hash and is passed directly to the constructor.
19
+ def wrap(variator)
20
+ case variator
21
+ when self
22
+ variator
23
+ when String
24
+ decode variator
25
+ else
26
+ new variator
27
+ end
28
+ end
29
+
30
+ # Returns a Variation instance with the transformations that were encoded by +encode+.
31
+ def decode(key)
32
+ new ActiveStorage.verifier.verify(key, purpose: :variation)
33
+ end
34
+
35
+ # Returns a signed key for the +transformations+, which can be used to refer to a specific
36
+ # variation in a URL or combined key (like <tt>ActiveStorage::Variant#key</tt>).
37
+ def encode(transformations)
38
+ ActiveStorage.verifier.generate(transformations, purpose: :variation)
39
+ end
40
+ end
41
+
42
+ def initialize(transformations)
43
+ @transformations = transformations.deep_symbolize_keys
44
+ end
45
+
46
+ # Accepts a File object, performs the +transformations+ against it, and
47
+ # saves the transformed image into a temporary file. If +format+ is specified
48
+ # it will be the format of the result image, otherwise the result image
49
+ # retains the source format.
50
+ def transform(file, format: nil, &block)
51
+ ActiveSupport::Notifications.instrument("transform.active_storage") do
52
+ transformer.transform(file, format: format, &block)
53
+ end
54
+ end
55
+
56
+ # Returns a signed key for all the +transformations+ that this variation was instantiated with.
57
+ def key
58
+ self.class.encode(transformations)
59
+ end
60
+
61
+ private
62
+ def transformer
63
+ if ActiveStorage.variant_processor
64
+ begin
65
+ require "image_processing"
66
+ rescue LoadError
67
+ ActiveSupport::Deprecation.warn <<~WARNING.squish
68
+ Generating image variants will require the image_processing gem in Rails 6.1.
69
+ Please add `gem 'image_processing', '~> 1.2'` to your Gemfile.
70
+ WARNING
71
+
72
+ ActiveStorage::Transformers::MiniMagickTransformer.new(transformations)
73
+ else
74
+ ActiveStorage::Transformers::ImageProcessingTransformer.new(transformations)
75
+ end
76
+ else
77
+ ActiveStorage::Transformers::MiniMagickTransformer.new(transformations)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.routes.draw do
4
+ scope ActiveStorage.routes_prefix do
5
+ get "/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
6
+
7
+ get "/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations#show", as: :rails_blob_representation
8
+
9
+ get "/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
10
+ put "/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
11
+ post "/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
12
+ end
13
+
14
+ direct :rails_representation do |representation, options|
15
+ signed_blob_id = representation.blob.signed_id
16
+ variation_key = representation.variation.key
17
+ filename = representation.blob.filename
18
+
19
+ route_for(:rails_blob_representation, signed_blob_id, variation_key, filename, options)
20
+ end
21
+
22
+ resolve("ActiveStorage::Variant") { |variant, options| route_for(:rails_representation, variant, options) }
23
+ resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_representation, preview, options) }
24
+
25
+
26
+ direct :rails_blob do |blob, options|
27
+ route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
28
+ end
29
+
30
+ resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) }
31
+ resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
32
+ end
@@ -0,0 +1,26 @@
1
+ class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
2
+ def change
3
+ create_table :active_storage_blobs do |t|
4
+ t.string :key, null: false
5
+ t.string :filename, null: false
6
+ t.string :content_type
7
+ t.text :metadata
8
+ t.bigint :byte_size, null: false
9
+ t.string :checksum, null: false
10
+ t.datetime :created_at, null: false
11
+
12
+ t.index [ :key ], unique: true
13
+ end
14
+
15
+ create_table :active_storage_attachments do |t|
16
+ t.string :name, null: false
17
+ t.references :record, null: false, polymorphic: true, index: false
18
+ t.references :blob, null: false
19
+
20
+ t.datetime :created_at, null: false
21
+
22
+ t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
23
+ t.foreign_key :active_storage_blobs, column: :blob_id
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+ class AddForeignKeyConstraintToActiveStorageAttachmentsForBlobId < ActiveRecord::Migration[6.0]
2
+ def up
3
+ return if foreign_key_exists?(:active_storage_attachments, column: :blob_id)
4
+
5
+ if table_exists?(:active_storage_blobs)
6
+ add_foreign_key :active_storage_attachments, :active_storage_blobs, column: :blob_id
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # Copyright (c) 2017-2019 David Heinemeier Hansson, Basecamp
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining
7
+ # a copy of this software and associated documentation files (the
8
+ # "Software"), to deal in the Software without restriction, including
9
+ # without limitation the rights to use, copy, modify, merge, publish,
10
+ # distribute, sublicense, and/or sell copies of the Software, and to
11
+ # permit persons to whom the Software is furnished to do so, subject to
12
+ # the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ #++
25
+
26
+ require "active_record"
27
+ require "active_support"
28
+ require "active_support/rails"
29
+ require "active_support/core_ext/numeric/time"
30
+
31
+ require "active_storage/version"
32
+ require "active_storage/errors"
33
+
34
+ require "marcel"
35
+
36
+ module ActiveStorage
37
+ extend ActiveSupport::Autoload
38
+
39
+ autoload :Attached
40
+ autoload :Service
41
+ autoload :Previewer
42
+ autoload :Analyzer
43
+
44
+ mattr_accessor :logger
45
+ mattr_accessor :verifier
46
+ mattr_accessor :variant_processor, default: :mini_magick
47
+
48
+ mattr_accessor :queues, default: {}
49
+
50
+ mattr_accessor :previewers, default: []
51
+ mattr_accessor :analyzers, default: []
52
+
53
+ mattr_accessor :paths, default: {}
54
+
55
+ mattr_accessor :variable_content_types, default: []
56
+ mattr_accessor :binary_content_type, default: "application/octet-stream"
57
+ mattr_accessor :content_types_to_serve_as_binary, default: []
58
+ mattr_accessor :content_types_allowed_inline, default: []
59
+
60
+ mattr_accessor :service_urls_expire_in, default: 5.minutes
61
+
62
+ mattr_accessor :routes_prefix, default: "/rails/active_storage"
63
+
64
+ mattr_accessor :replace_on_assign_to_many, default: false
65
+
66
+ module Transformers
67
+ extend ActiveSupport::Autoload
68
+
69
+ autoload :Transformer
70
+ autoload :ImageProcessingTransformer
71
+ autoload :MiniMagickTransformer
72
+ end
73
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ # This is an abstract base class for analyzers, which extract metadata from blobs. See
5
+ # ActiveStorage::Analyzer::ImageAnalyzer for an example of a concrete subclass.
6
+ class Analyzer
7
+ attr_reader :blob
8
+
9
+ # Implement this method in a concrete subclass. Have it return true when given a blob from which
10
+ # the analyzer can extract metadata.
11
+ def self.accept?(blob)
12
+ false
13
+ end
14
+
15
+ def initialize(blob)
16
+ @blob = blob
17
+ end
18
+
19
+ # Override this method in a concrete subclass. Have it return a Hash of metadata.
20
+ def metadata
21
+ raise NotImplementedError
22
+ end
23
+
24
+ private
25
+ # Downloads the blob to a tempfile on disk. Yields the tempfile.
26
+ def download_blob_to_tempfile(&block) #:doc:
27
+ blob.open tmpdir: tmpdir, &block
28
+ end
29
+
30
+ def logger #:doc:
31
+ ActiveStorage.logger
32
+ end
33
+
34
+ def tmpdir #:doc:
35
+ Dir.tmpdir
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ # Extracts width and height in pixels from an image blob.
5
+ #
6
+ # If the image contains EXIF data indicating its angle is 90 or 270 degrees, its width and height are swapped for convenience.
7
+ #
8
+ # Example:
9
+ #
10
+ # ActiveStorage::Analyzer::ImageAnalyzer.new(blob).metadata
11
+ # # => { width: 4104, height: 2736 }
12
+ #
13
+ # This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires
14
+ # the {ImageMagick}[http://www.imagemagick.org] system library.
15
+ class Analyzer::ImageAnalyzer < Analyzer
16
+ def self.accept?(blob)
17
+ blob.image?
18
+ end
19
+
20
+ def metadata
21
+ read_image do |image|
22
+ if rotated_image?(image)
23
+ { width: image.height, height: image.width }
24
+ else
25
+ { width: image.width, height: image.height }
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+ def read_image
32
+ download_blob_to_tempfile do |file|
33
+ require "mini_magick"
34
+ image = MiniMagick::Image.new(file.path)
35
+
36
+ if image.valid?
37
+ yield image
38
+ else
39
+ logger.info "Skipping image analysis because ImageMagick doesn't support the file"
40
+ {}
41
+ end
42
+ end
43
+ rescue LoadError
44
+ logger.info "Skipping image analysis because the mini_magick gem isn't installed"
45
+ {}
46
+ end
47
+
48
+ def rotated_image?(image)
49
+ %w[ RightTop LeftBottom ].include?(image["%[orientation]"])
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class Analyzer::NullAnalyzer < Analyzer # :nodoc:
5
+ def self.accept?(blob)
6
+ true
7
+ end
8
+
9
+ def metadata
10
+ {}
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ # Extracts the following from a video blob:
5
+ #
6
+ # * Width (pixels)
7
+ # * Height (pixels)
8
+ # * Duration (seconds)
9
+ # * Angle (degrees)
10
+ # * Display aspect ratio
11
+ #
12
+ # Example:
13
+ #
14
+ # ActiveStorage::Analyzer::VideoAnalyzer.new(blob).metadata
15
+ # # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3] }
16
+ #
17
+ # When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience.
18
+ #
19
+ # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
20
+ class Analyzer::VideoAnalyzer < Analyzer
21
+ def self.accept?(blob)
22
+ blob.video?
23
+ end
24
+
25
+ def metadata
26
+ { width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio }.compact
27
+ end
28
+
29
+ private
30
+ def width
31
+ if rotated?
32
+ computed_height || encoded_height
33
+ else
34
+ encoded_width
35
+ end
36
+ end
37
+
38
+ def height
39
+ if rotated?
40
+ encoded_width
41
+ else
42
+ computed_height || encoded_height
43
+ end
44
+ end
45
+
46
+ def duration
47
+ Float(video_stream["duration"]) if video_stream["duration"]
48
+ end
49
+
50
+ def angle
51
+ Integer(tags["rotate"]) if tags["rotate"]
52
+ end
53
+
54
+ def display_aspect_ratio
55
+ if descriptor = video_stream["display_aspect_ratio"]
56
+ if terms = descriptor.split(":", 2)
57
+ numerator = Integer(terms[0])
58
+ denominator = Integer(terms[1])
59
+
60
+ [numerator, denominator] unless numerator == 0
61
+ end
62
+ end
63
+ end
64
+
65
+
66
+ def rotated?
67
+ angle == 90 || angle == 270
68
+ end
69
+
70
+ def computed_height
71
+ if encoded_width && display_height_scale
72
+ encoded_width * display_height_scale
73
+ end
74
+ end
75
+
76
+ def encoded_width
77
+ @encoded_width ||= Float(video_stream["width"]) if video_stream["width"]
78
+ end
79
+
80
+ def encoded_height
81
+ @encoded_height ||= Float(video_stream["height"]) if video_stream["height"]
82
+ end
83
+
84
+ def display_height_scale
85
+ @display_height_scale ||= Float(display_aspect_ratio.last) / display_aspect_ratio.first if display_aspect_ratio
86
+ end
87
+
88
+
89
+ def tags
90
+ @tags ||= video_stream["tags"] || {}
91
+ end
92
+
93
+ def video_stream
94
+ @video_stream ||= streams.detect { |stream| stream["codec_type"] == "video" } || {}
95
+ end
96
+
97
+ def streams
98
+ probe["streams"] || []
99
+ end
100
+
101
+ def probe
102
+ download_blob_to_tempfile { |file| probe_from(file) }
103
+ end
104
+
105
+ def probe_from(file)
106
+ IO.popen([ ffprobe_path, "-print_format", "json", "-show_streams", "-v", "error", file.path ]) do |output|
107
+ JSON.parse(output.read)
108
+ end
109
+ rescue Errno::ENOENT
110
+ logger.info "Skipping video analysis because FFmpeg isn't installed"
111
+ {}
112
+ end
113
+
114
+ def ffprobe_path
115
+ ActiveStorage.paths[:ffprobe] || "ffprobe"
116
+ end
117
+ end
118
+ end