activestorage 0.1 → 5.2.0.beta1

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/README.md +94 -25
  4. data/app/assets/javascripts/activestorage.js +1 -0
  5. data/app/controllers/active_storage/blobs_controller.rb +16 -0
  6. data/app/controllers/active_storage/direct_uploads_controller.rb +23 -0
  7. data/app/controllers/active_storage/disk_controller.rb +51 -0
  8. data/app/controllers/active_storage/previews_controller.rb +12 -0
  9. data/app/controllers/active_storage/variants_controller.rb +16 -0
  10. data/app/javascript/activestorage/blob_record.js +54 -0
  11. data/app/javascript/activestorage/blob_upload.js +35 -0
  12. data/app/javascript/activestorage/direct_upload.js +42 -0
  13. data/app/javascript/activestorage/direct_upload_controller.js +67 -0
  14. data/app/javascript/activestorage/direct_uploads_controller.js +50 -0
  15. data/app/javascript/activestorage/file_checksum.js +53 -0
  16. data/app/javascript/activestorage/helpers.js +42 -0
  17. data/app/javascript/activestorage/index.js +11 -0
  18. data/app/javascript/activestorage/ujs.js +75 -0
  19. data/app/jobs/active_storage/analyze_job.rb +8 -0
  20. data/app/jobs/active_storage/base_job.rb +5 -0
  21. data/app/jobs/active_storage/purge_job.rb +11 -0
  22. data/app/models/active_storage/attachment.rb +35 -0
  23. data/app/models/active_storage/blob.rb +313 -0
  24. data/app/models/active_storage/filename.rb +73 -0
  25. data/app/models/active_storage/filename/parameters.rb +36 -0
  26. data/app/models/active_storage/preview.rb +90 -0
  27. data/app/models/active_storage/variant.rb +86 -0
  28. data/app/models/active_storage/variation.rb +67 -0
  29. data/config/routes.rb +43 -0
  30. data/lib/active_storage.rb +37 -2
  31. data/lib/active_storage/analyzer.rb +33 -0
  32. data/lib/active_storage/analyzer/image_analyzer.rb +36 -0
  33. data/lib/active_storage/analyzer/null_analyzer.rb +13 -0
  34. data/lib/active_storage/analyzer/video_analyzer.rb +79 -0
  35. data/lib/active_storage/attached.rb +28 -22
  36. data/lib/active_storage/attached/macros.rb +89 -16
  37. data/lib/active_storage/attached/many.rb +53 -21
  38. data/lib/active_storage/attached/one.rb +74 -20
  39. data/lib/active_storage/downloading.rb +26 -0
  40. data/lib/active_storage/engine.rb +72 -0
  41. data/lib/active_storage/gem_version.rb +17 -0
  42. data/lib/active_storage/log_subscriber.rb +52 -0
  43. data/lib/active_storage/previewer.rb +58 -0
  44. data/lib/active_storage/previewer/pdf_previewer.rb +17 -0
  45. data/lib/active_storage/previewer/video_previewer.rb +23 -0
  46. data/lib/active_storage/service.rb +112 -24
  47. data/lib/active_storage/service/azure_storage_service.rb +124 -0
  48. data/lib/active_storage/service/configurator.rb +32 -0
  49. data/lib/active_storage/service/disk_service.rb +103 -44
  50. data/lib/active_storage/service/gcs_service.rb +87 -29
  51. data/lib/active_storage/service/mirror_service.rb +38 -22
  52. data/lib/active_storage/service/s3_service.rb +83 -38
  53. data/lib/active_storage/version.rb +10 -0
  54. data/lib/tasks/activestorage.rake +4 -15
  55. metadata +64 -108
  56. data/.gitignore +0 -1
  57. data/Gemfile +0 -11
  58. data/Gemfile.lock +0 -235
  59. data/Rakefile +0 -11
  60. data/activestorage.gemspec +0 -21
  61. data/lib/active_storage/attachment.rb +0 -30
  62. data/lib/active_storage/blob.rb +0 -80
  63. data/lib/active_storage/disk_controller.rb +0 -28
  64. data/lib/active_storage/download.rb +0 -90
  65. data/lib/active_storage/filename.rb +0 -31
  66. data/lib/active_storage/migration.rb +0 -28
  67. data/lib/active_storage/purge_job.rb +0 -10
  68. data/lib/active_storage/railtie.rb +0 -56
  69. data/lib/active_storage/storage_services.yml +0 -27
  70. data/lib/active_storage/verified_key_with_expiration.rb +0 -24
  71. data/test/attachments_test.rb +0 -95
  72. data/test/blob_test.rb +0 -28
  73. data/test/database/create_users_migration.rb +0 -7
  74. data/test/database/setup.rb +0 -6
  75. data/test/disk_controller_test.rb +0 -34
  76. data/test/filename_test.rb +0 -36
  77. data/test/service/.gitignore +0 -1
  78. data/test/service/configurations-example.yml +0 -11
  79. data/test/service/disk_service_test.rb +0 -8
  80. data/test/service/gcs_service_test.rb +0 -20
  81. data/test/service/mirror_service_test.rb +0 -50
  82. data/test/service/s3_service_test.rb +0 -11
  83. data/test/service/shared_service_tests.rb +0 -68
  84. data/test/test_helper.rb +0 -28
  85. data/test/verified_key_with_expiration_test.rb +0 -19
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_storage/downloading"
4
+
5
+ module ActiveStorage
6
+ # This is an abstract base class for analyzers, which extract metadata from blobs. See
7
+ # ActiveStorage::Analyzer::ImageAnalyzer for an example of a concrete subclass.
8
+ class Analyzer
9
+ include Downloading
10
+
11
+ attr_reader :blob
12
+
13
+ # Implement this method in a concrete subclass. Have it return true when given a blob from which
14
+ # the analyzer can extract metadata.
15
+ def self.accept?(blob)
16
+ false
17
+ end
18
+
19
+ def initialize(blob)
20
+ @blob = blob
21
+ end
22
+
23
+ # Override this method in a concrete subclass. Have it return a Hash of metadata.
24
+ def metadata
25
+ raise NotImplementedError
26
+ end
27
+
28
+ private
29
+ def logger
30
+ ActiveStorage.logger
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ # Extracts width and height in pixels from an image blob.
5
+ #
6
+ # Example:
7
+ #
8
+ # ActiveStorage::Analyzer::ImageAnalyzer.new(blob).metadata
9
+ # # => { width: 4104, height: 2736 }
10
+ #
11
+ # This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires
12
+ # the {ImageMagick}[http://www.imagemagick.org] system library. These libraries are not provided by Rails; you must
13
+ # install them yourself to use this analyzer.
14
+ class Analyzer::ImageAnalyzer < Analyzer
15
+ def self.accept?(blob)
16
+ blob.image?
17
+ end
18
+
19
+ def metadata
20
+ read_image do |image|
21
+ { width: image.width, height: image.height }
22
+ end
23
+ rescue LoadError
24
+ logger.info "Skipping image analysis because the mini_magick gem isn't installed"
25
+ {}
26
+ end
27
+
28
+ private
29
+ def read_image
30
+ download_blob_to_tempfile do |file|
31
+ require "mini_magick"
32
+ yield MiniMagick::Image.new(file.path)
33
+ end
34
+ end
35
+ end
36
+ 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,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/compact"
4
+
5
+ module ActiveStorage
6
+ # Extracts the following from a video blob:
7
+ #
8
+ # * Width (pixels)
9
+ # * Height (pixels)
10
+ # * Duration (seconds)
11
+ # * Angle (degrees)
12
+ # * Aspect ratio
13
+ #
14
+ # Example:
15
+ #
16
+ # ActiveStorage::VideoAnalyzer.new(blob).metadata
17
+ # # => { width: 640, height: 480, duration: 5.0, angle: 0, aspect_ratio: [4, 3] }
18
+ #
19
+ # This analyzer requires the {ffmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails. You must
20
+ # install ffmpeg yourself to use this analyzer.
21
+ class Analyzer::VideoAnalyzer < Analyzer
22
+ def self.accept?(blob)
23
+ blob.video?
24
+ end
25
+
26
+ def metadata
27
+ { width: width, height: height, duration: duration, angle: angle, aspect_ratio: aspect_ratio }.compact
28
+ end
29
+
30
+ private
31
+ def width
32
+ Integer(video_stream["width"]) if video_stream["width"]
33
+ end
34
+
35
+ def height
36
+ Integer(video_stream["height"]) if video_stream["height"]
37
+ end
38
+
39
+ def duration
40
+ Float(video_stream["duration"]) if video_stream["duration"]
41
+ end
42
+
43
+ def angle
44
+ Integer(tags["rotate"]) if tags["rotate"]
45
+ end
46
+
47
+ def aspect_ratio
48
+ if descriptor = video_stream["display_aspect_ratio"]
49
+ descriptor.split(":", 2).collect(&:to_i)
50
+ end
51
+ end
52
+
53
+
54
+ def tags
55
+ @tags ||= video_stream["tags"] || {}
56
+ end
57
+
58
+ def video_stream
59
+ @video_stream ||= streams.detect { |stream| stream["codec_type"] == "video" } || {}
60
+ end
61
+
62
+ def streams
63
+ probe["streams"] || []
64
+ end
65
+
66
+ def probe
67
+ download_blob_to_tempfile { |file| probe_from(file) }
68
+ end
69
+
70
+ def probe_from(file)
71
+ IO.popen([ "ffprobe", "-print_format", "json", "-show_streams", "-v", "error", file.path ]) do |output|
72
+ JSON.parse(output.read)
73
+ end
74
+ rescue Errno::ENOENT
75
+ logger.info "Skipping video analysis because ffmpeg isn't installed"
76
+ {}
77
+ end
78
+ end
79
+ end
@@ -1,32 +1,38 @@
1
- require "active_storage/blob"
2
- require "active_storage/attachment"
1
+ # frozen_string_literal: true
3
2
 
3
+ require "action_dispatch"
4
4
  require "action_dispatch/http/upload"
5
5
  require "active_support/core_ext/module/delegation"
6
6
 
7
- class ActiveStorage::Attached
8
- attr_reader :name, :record
7
+ module ActiveStorage
8
+ # Abstract base class for the concrete ActiveStorage::Attached::One and ActiveStorage::Attached::Many
9
+ # classes that both provide proxy access to the blob association for a record.
10
+ class Attached
11
+ attr_reader :name, :record, :dependent
9
12
 
10
- def initialize(name, record)
11
- @name, @record = name, record
12
- end
13
+ def initialize(name, record, dependent:)
14
+ @name, @record, @dependent = name, record, dependent
15
+ end
13
16
 
14
- private
15
- def create_blob_from(attachable)
16
- case attachable
17
- when ActiveStorage::Blob
18
- attachable
19
- when ActionDispatch::Http::UploadedFile
20
- ActiveStorage::Blob.create_after_upload! \
21
- io: attachable.open,
22
- filename: attachable.original_filename,
23
- content_type: attachable.content_type
24
- when Hash
25
- ActiveStorage::Blob.create_after_upload!(attachable)
26
- else
27
- nil
17
+ private
18
+ def create_blob_from(attachable)
19
+ case attachable
20
+ when ActiveStorage::Blob
21
+ attachable
22
+ when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
23
+ ActiveStorage::Blob.create_after_upload! \
24
+ io: attachable.open,
25
+ filename: attachable.original_filename,
26
+ content_type: attachable.content_type
27
+ when Hash
28
+ ActiveStorage::Blob.create_after_upload!(attachable)
29
+ when String
30
+ ActiveStorage::Blob.find_signed(attachable)
31
+ else
32
+ nil
33
+ end
28
34
  end
29
- end
35
+ end
30
36
  end
31
37
 
32
38
  require "active_storage/attached/one"
@@ -1,23 +1,96 @@
1
- module ActiveStorage::Attached::Macros
2
- def has_one_attached(name, dependent: :purge_later)
3
- define_method(name) do
4
- instance_variable_get("@active_storage_attached_#{name}") ||
5
- instance_variable_set("@active_storage_attached_#{name}", ActiveStorage::Attached::One.new(name, self))
6
- end
1
+ # frozen_string_literal: true
7
2
 
8
- if dependent == :purge_later
9
- before_destroy { public_send(name).purge_later }
10
- end
11
- end
3
+ module ActiveStorage
4
+ # Provides the class-level DSL for declaring that an Active Record model has attached blobs.
5
+ module Attached::Macros
6
+ # Specifies the relation between a single attachment and the model.
7
+ #
8
+ # class User < ActiveRecord::Base
9
+ # has_one_attached :avatar
10
+ # end
11
+ #
12
+ # There is no column defined on the model side, Active Storage takes
13
+ # care of the mapping between your records and the attachment.
14
+ #
15
+ # To avoid N+1 queries, you can include the attached blobs in your query like so:
16
+ #
17
+ # User.with_attached_avatar
18
+ #
19
+ # Under the covers, this relationship is implemented as a +has_one+ association to a
20
+ # ActiveStorage::Attachment record and a +has_one-through+ association to a
21
+ # ActiveStorage::Blob record. These associations are available as +avatar_attachment+
22
+ # and +avatar_blob+. But you shouldn't need to work with these associations directly in
23
+ # most circumstances.
24
+ #
25
+ # The system has been designed to having you go through the ActiveStorage::Attached::One
26
+ # proxy that provides the dynamic proxy to the associations and factory methods, like +attach+.
27
+ #
28
+ # If the +:dependent+ option isn't set, the attachment will be purged
29
+ # (i.e. destroyed) whenever the record is destroyed.
30
+ def has_one_attached(name, dependent: :purge_later)
31
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
32
+ def #{name}
33
+ @active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self, dependent: #{dependent == :purge_later ? ":purge_later" : "false"})
34
+ end
35
+
36
+ def #{name}=(attachable)
37
+ #{name}.attach(attachable)
38
+ end
39
+ CODE
40
+
41
+ has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record
42
+ has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob
12
43
 
13
- def has_many_attached(name, dependent: :purge_later)
14
- define_method(name) do
15
- instance_variable_get("@active_storage_attached_#{name}") ||
16
- instance_variable_set("@active_storage_attached_#{name}", ActiveStorage::Attached::Many.new(name, self))
44
+ scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) }
45
+
46
+ if dependent == :purge_later
47
+ before_destroy { public_send(name).purge_later }
48
+ end
17
49
  end
18
50
 
19
- if dependent == :purge_later
20
- before_destroy { public_send(name).purge_later }
51
+ # Specifies the relation between multiple attachments and the model.
52
+ #
53
+ # class Gallery < ActiveRecord::Base
54
+ # has_many_attached :photos
55
+ # end
56
+ #
57
+ # There are no columns defined on the model side, Active Storage takes
58
+ # care of the mapping between your records and the attachments.
59
+ #
60
+ # To avoid N+1 queries, you can include the attached blobs in your query like so:
61
+ #
62
+ # Gallery.where(user: Current.user).with_attached_photos
63
+ #
64
+ # Under the covers, this relationship is implemented as a +has_many+ association to a
65
+ # ActiveStorage::Attachment record and a +has_many-through+ association to a
66
+ # ActiveStorage::Blob record. These associations are available as +photos_attachments+
67
+ # and +photos_blobs+. But you shouldn't need to work with these associations directly in
68
+ # most circumstances.
69
+ #
70
+ # The system has been designed to having you go through the ActiveStorage::Attached::Many
71
+ # proxy that provides the dynamic proxy to the associations and factory methods, like +#attach+.
72
+ #
73
+ # If the +:dependent+ option isn't set, all the attachments will be purged
74
+ # (i.e. destroyed) whenever the record is destroyed.
75
+ def has_many_attached(name, dependent: :purge_later)
76
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
77
+ def #{name}
78
+ @active_storage_attached_#{name} ||= ActiveStorage::Attached::Many.new("#{name}", self, dependent: #{dependent == :purge_later ? ":purge_later" : "false"})
79
+ end
80
+
81
+ def #{name}=(attachables)
82
+ #{name}.attach(attachables)
83
+ end
84
+ CODE
85
+
86
+ has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment"
87
+ has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob
88
+
89
+ scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) }
90
+
91
+ if dependent == :purge_later
92
+ before_destroy { public_send(name).purge_later }
93
+ end
21
94
  end
22
95
  end
23
96
  end
@@ -1,31 +1,63 @@
1
- class ActiveStorage::Attached::Many < ActiveStorage::Attached
2
- delegate_missing_to :attachments
1
+ # frozen_string_literal: true
3
2
 
4
- def attachments
5
- @attachments ||= ActiveStorage::Attachment.where(record_gid: record.to_gid.to_s, name: name)
6
- end
3
+ module ActiveStorage
4
+ # Decorated proxy object representing of multiple attachments to a model.
5
+ class Attached::Many < Attached
6
+ delegate_missing_to :attachments
7
7
 
8
- def attach(*attachables)
9
- @attachments = attachments | Array(attachables).flatten.collect do |attachable|
10
- ActiveStorage::Attachment.create!(record_gid: record.to_gid.to_s, name: name, blob: create_blob_from(attachable))
8
+ # Returns all the associated attachment records.
9
+ #
10
+ # All methods called on this proxy object that aren't listed here will automatically be delegated to +attachments+.
11
+ def attachments
12
+ record.public_send("#{name}_attachments")
11
13
  end
12
- end
13
14
 
14
- def attached?
15
- attachments.any?
16
- end
15
+ # Associates one or several attachments with the current record, saving them to the database.
16
+ #
17
+ # document.images.attach(params[:images]) # Array of ActionDispatch::Http::UploadedFile objects
18
+ # document.images.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
19
+ # document.images.attach(io: File.open("/path/to/racecar.jpg"), filename: "racecar.jpg", content_type: "image/jpg")
20
+ # document.images.attach([ first_blob, second_blob ])
21
+ def attach(*attachables)
22
+ attachables.flatten.collect do |attachable|
23
+ if record.new_record?
24
+ attachments.build(record: record, blob: create_blob_from(attachable))
25
+ else
26
+ attachments.create!(record: record, blob: create_blob_from(attachable))
27
+ end
28
+ end
29
+ end
17
30
 
18
- def purge
19
- if attached?
20
- attachments.each(&:purge)
21
- @attachments = nil
31
+ # Returns true if any attachments has been made.
32
+ #
33
+ # class Gallery < ActiveRecord::Base
34
+ # has_many_attached :photos
35
+ # end
36
+ #
37
+ # Gallery.new.photos.attached? # => false
38
+ def attached?
39
+ attachments.any?
40
+ end
41
+
42
+ # Deletes associated attachments without purging them, leaving their respective blobs in place.
43
+ def detach
44
+ attachments.destroy_all if attached?
45
+ end
46
+
47
+ # Directly purges each associated attachment (i.e. destroys the blobs and
48
+ # attachments and deletes the files on the service).
49
+ def purge
50
+ if attached?
51
+ attachments.each(&:purge)
52
+ attachments.reload
53
+ end
22
54
  end
23
- end
24
55
 
25
- def purge_later
26
- if attached?
27
- attachments.each(&:purge_later)
28
- @attachments = nil
56
+ # Purges each associated attachment through the queuing system.
57
+ def purge_later
58
+ if attached?
59
+ attachments.each(&:purge_later)
60
+ end
29
61
  end
30
62
  end
31
63
  end
@@ -1,29 +1,83 @@
1
- class ActiveStorage::Attached::One < ActiveStorage::Attached
2
- delegate_missing_to :attachment
1
+ # frozen_string_literal: true
3
2
 
4
- def attachment
5
- @attachment ||= ActiveStorage::Attachment.find_by(record_gid: record.to_gid.to_s, name: name)
6
- end
3
+ module ActiveStorage
4
+ # Representation of a single attachment to a model.
5
+ class Attached::One < Attached
6
+ delegate_missing_to :attachment
7
7
 
8
- def attach(attachable)
9
- @attachment = ActiveStorage::Attachment.create!(record_gid: record.to_gid.to_s, name: name, blob: create_blob_from(attachable))
10
- end
8
+ # Returns the associated attachment record.
9
+ #
10
+ # You don't have to call this method to access the attachment's methods as
11
+ # they are all available at the model level.
12
+ def attachment
13
+ record.public_send("#{name}_attachment")
14
+ end
11
15
 
12
- def attached?
13
- attachment.present?
14
- end
16
+ # Associates a given attachment with the current record, saving it to the database.
17
+ #
18
+ # person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object
19
+ # person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
20
+ # person.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg")
21
+ # person.avatar.attach(avatar_blob) # ActiveStorage::Blob object
22
+ def attach(attachable)
23
+ if attached? && dependent == :purge_later
24
+ replace attachable
25
+ else
26
+ write_attachment build_attachment_from(attachable)
27
+ end
28
+ end
15
29
 
16
- def purge
17
- if attached?
18
- attachment.purge
19
- @attachment = nil
30
+ # Returns +true+ if an attachment has been made.
31
+ #
32
+ # class User < ActiveRecord::Base
33
+ # has_one_attached :avatar
34
+ # end
35
+ #
36
+ # User.new.avatar.attached? # => false
37
+ def attached?
38
+ attachment.present?
20
39
  end
21
- end
22
40
 
23
- def purge_later
24
- if attached?
25
- attachment.purge_later
26
- @attachment = nil
41
+ # Deletes the attachment without purging it, leaving its blob in place.
42
+ def detach
43
+ if attached?
44
+ attachment.destroy
45
+ write_attachment nil
46
+ end
27
47
  end
48
+
49
+ # Directly purges the attachment (i.e. destroys the blob and
50
+ # attachment and deletes the file on the service).
51
+ def purge
52
+ if attached?
53
+ attachment.purge
54
+ write_attachment nil
55
+ end
56
+ end
57
+
58
+ # Purges the attachment through the queuing system.
59
+ def purge_later
60
+ if attached?
61
+ attachment.purge_later
62
+ end
63
+ end
64
+
65
+ private
66
+ def replace(attachable)
67
+ blob.tap do
68
+ transaction do
69
+ detach
70
+ write_attachment build_attachment_from(attachable)
71
+ end
72
+ end.purge_later
73
+ end
74
+
75
+ def build_attachment_from(attachable)
76
+ ActiveStorage::Attachment.new(record: record, name: name, blob: create_blob_from(attachable))
77
+ end
78
+
79
+ def write_attachment(attachment)
80
+ record.public_send("#{name}_attachment=", attachment)
81
+ end
28
82
  end
29
83
  end