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,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class Previewer::PopplerPDFPreviewer < Previewer
5
+ class << self
6
+ def accept?(blob)
7
+ blob.content_type == "application/pdf" && pdftoppm_exists?
8
+ end
9
+
10
+ def pdftoppm_path
11
+ ActiveStorage.paths[:pdftoppm] || "pdftoppm"
12
+ end
13
+
14
+ def pdftoppm_exists?
15
+ return @pdftoppm_exists if defined?(@pdftoppm_exists)
16
+
17
+ @pdftoppm_exists = system(pdftoppm_path, "-v", out: File::NULL, err: File::NULL)
18
+ end
19
+ end
20
+
21
+ def preview
22
+ download_blob_to_tempfile do |input|
23
+ draw_first_page_from input do |output|
24
+ yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+ def draw_first_page_from(file, &block)
31
+ # use 72 dpi to match thumbnail dimensions of the PDF
32
+ draw self.class.pdftoppm_path, "-singlefile", "-r", "72", "-png", file.path, &block
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class Previewer::VideoPreviewer < Previewer
5
+ def self.accept?(blob)
6
+ blob.video?
7
+ end
8
+
9
+ def preview
10
+ download_blob_to_tempfile do |input|
11
+ draw_relevant_frame_from input do |output|
12
+ yield io: output, filename: "#{blob.filename.base}.jpg", content_type: "image/jpeg"
13
+ end
14
+ end
15
+ end
16
+
17
+ private
18
+ def draw_relevant_frame_from(file, &block)
19
+ draw ffmpeg_path, "-i", file.path, "-y", "-vframes", "1", "-f", "image2", "-", &block
20
+ end
21
+
22
+ def ffmpeg_path
23
+ ActiveStorage.paths[:ffmpeg] || "ffmpeg"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ module Reflection
5
+ # Holds all the metadata about a has_one_attached attachment as it was
6
+ # specified in the Active Record class.
7
+ class HasOneAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc:
8
+ def macro
9
+ :has_one_attached
10
+ end
11
+ end
12
+
13
+ # Holds all the metadata about a has_many_attached attachment as it was
14
+ # specified in the Active Record class.
15
+ class HasManyAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc:
16
+ def macro
17
+ :has_many_attached
18
+ end
19
+ end
20
+
21
+ module ReflectionExtension # :nodoc:
22
+ def add_attachment_reflection(model, name, reflection)
23
+ model.attachment_reflections = model.attachment_reflections.merge(name.to_s => reflection)
24
+ end
25
+
26
+ private
27
+ def reflection_class_for(macro)
28
+ case macro
29
+ when :has_one_attached
30
+ HasOneAttachedReflection
31
+ when :has_many_attached
32
+ HasManyAttachedReflection
33
+ else
34
+ super
35
+ end
36
+ end
37
+ end
38
+
39
+ module ActiveRecordExtensions
40
+ extend ActiveSupport::Concern
41
+
42
+ included do
43
+ class_attribute :attachment_reflections, instance_writer: false, default: {}
44
+ end
45
+
46
+ module ClassMethods
47
+ # Returns an array of reflection objects for all the attachments in the
48
+ # class.
49
+ def reflect_on_all_attachments
50
+ attachment_reflections.values
51
+ end
52
+
53
+ # Returns the reflection object for the named +attachment+.
54
+ #
55
+ # User.reflect_on_attachment(:avatar)
56
+ # # => the avatar reflection
57
+ #
58
+ def reflect_on_attachment(attachment)
59
+ attachment_reflections[attachment.to_s]
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_storage/log_subscriber"
4
+ require "action_dispatch"
5
+ require "action_dispatch/http/content_disposition"
6
+
7
+ module ActiveStorage
8
+ # Abstract class serving as an interface for concrete services.
9
+ #
10
+ # The available services are:
11
+ #
12
+ # * +Disk+, to manage attachments saved directly on the hard drive.
13
+ # * +GCS+, to manage attachments through Google Cloud Storage.
14
+ # * +S3+, to manage attachments through Amazon S3.
15
+ # * +AzureStorage+, to manage attachments through Microsoft Azure Storage.
16
+ # * +Mirror+, to be able to use several services to manage attachments.
17
+ #
18
+ # Inside a Rails application, you can set-up your services through the
19
+ # generated <tt>config/storage.yml</tt> file and reference one
20
+ # of the aforementioned constant under the +service+ key. For example:
21
+ #
22
+ # local:
23
+ # service: Disk
24
+ # root: <%= Rails.root.join("storage") %>
25
+ #
26
+ # You can checkout the service's constructor to know which keys are required.
27
+ #
28
+ # Then, in your application's configuration, you can specify the service to
29
+ # use like this:
30
+ #
31
+ # config.active_storage.service = :local
32
+ #
33
+ # If you are using Active Storage outside of a Ruby on Rails application, you
34
+ # can configure the service to use like this:
35
+ #
36
+ # ActiveStorage::Blob.service = ActiveStorage::Service.configure(
37
+ # :Disk,
38
+ # root: Pathname("/foo/bar/storage")
39
+ # )
40
+ class Service
41
+ extend ActiveSupport::Autoload
42
+ autoload :Configurator
43
+
44
+ class << self
45
+ # Configure an Active Storage service by name from a set of configurations,
46
+ # typically loaded from a YAML file. The Active Storage engine uses this
47
+ # to set the global Active Storage service when the app boots.
48
+ def configure(service_name, configurations)
49
+ Configurator.build(service_name, configurations)
50
+ end
51
+
52
+ # Override in subclasses that stitch together multiple services and hence
53
+ # need to build additional services using the configurator.
54
+ #
55
+ # Passes the configurator and all of the service's config as keyword args.
56
+ #
57
+ # See MirrorService for an example.
58
+ def build(configurator:, service: nil, **service_config) #:nodoc:
59
+ new(**service_config)
60
+ end
61
+ end
62
+
63
+ # Upload the +io+ to the +key+ specified. If a +checksum+ is provided, the service will
64
+ # ensure a match when the upload has completed or raise an ActiveStorage::IntegrityError.
65
+ def upload(key, io, checksum: nil, **options)
66
+ raise NotImplementedError
67
+ end
68
+
69
+ # Update metadata for the file identified by +key+ in the service.
70
+ # Override in subclasses only if the service needs to store specific
71
+ # metadata that has to be updated upon identification.
72
+ def update_metadata(key, **metadata)
73
+ end
74
+
75
+ # Return the content of the file at the +key+.
76
+ def download(key)
77
+ raise NotImplementedError
78
+ end
79
+
80
+ # Return the partial content in the byte +range+ of the file at the +key+.
81
+ def download_chunk(key, range)
82
+ raise NotImplementedError
83
+ end
84
+
85
+ def open(*args, &block)
86
+ ActiveStorage::Downloader.new(self).open(*args, &block)
87
+ end
88
+
89
+ # Delete the file at the +key+.
90
+ def delete(key)
91
+ raise NotImplementedError
92
+ end
93
+
94
+ # Delete files at keys starting with the +prefix+.
95
+ def delete_prefixed(prefix)
96
+ raise NotImplementedError
97
+ end
98
+
99
+ # Return +true+ if a file exists at the +key+.
100
+ def exist?(key)
101
+ raise NotImplementedError
102
+ end
103
+
104
+ # Returns a signed, temporary URL for the file at the +key+. The URL will be valid for the amount
105
+ # of seconds specified in +expires_in+. You must also provide the +disposition+ (+:inline+ or +:attachment+),
106
+ # +filename+, and +content_type+ that you wish the file to be served with on request.
107
+ def url(key, expires_in:, disposition:, filename:, content_type:)
108
+ raise NotImplementedError
109
+ end
110
+
111
+ # Returns a signed, temporary URL that a direct upload file can be PUT to on the +key+.
112
+ # The URL will be valid for the amount of seconds specified in +expires_in+.
113
+ # You must also provide the +content_type+, +content_length+, and +checksum+ of the file
114
+ # that will be uploaded. All these attributes will be validated by the service upon upload.
115
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
116
+ raise NotImplementedError
117
+ end
118
+
119
+ # Returns a Hash of headers for +url_for_direct_upload+ requests.
120
+ def headers_for_direct_upload(key, filename:, content_type:, content_length:, checksum:)
121
+ {}
122
+ end
123
+
124
+ private
125
+ def instrument(operation, payload = {}, &block)
126
+ ActiveSupport::Notifications.instrument(
127
+ "service_#{operation}.active_storage",
128
+ payload.merge(service: service_name), &block)
129
+ end
130
+
131
+ def service_name
132
+ # ActiveStorage::Service::DiskService => Disk
133
+ self.class.name.split("::").third.remove("Service")
134
+ end
135
+
136
+ def content_disposition_with(type: "inline", filename:)
137
+ disposition = (type.to_s.presence_in(%w( attachment inline )) || "inline")
138
+ ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename.sanitized)
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/numeric/bytes"
4
+ require "azure/storage"
5
+ require "azure/storage/core/auth/shared_access_signature"
6
+
7
+ module ActiveStorage
8
+ # Wraps the Microsoft Azure Storage Blob Service as an Active Storage service.
9
+ # See ActiveStorage::Service for the generic API documentation that applies to all services.
10
+ class Service::AzureStorageService < Service
11
+ attr_reader :client, :blobs, :container, :signer
12
+
13
+ def initialize(storage_account_name:, storage_access_key:, container:, **options)
14
+ @client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key, **options)
15
+ @signer = Azure::Storage::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
16
+ @blobs = client.blob_client
17
+ @container = container
18
+ end
19
+
20
+ def upload(key, io, checksum: nil, **)
21
+ instrument :upload, key: key, checksum: checksum do
22
+ handle_errors do
23
+ blobs.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum)
24
+ end
25
+ end
26
+ end
27
+
28
+ def download(key, &block)
29
+ if block_given?
30
+ instrument :streaming_download, key: key do
31
+ stream(key, &block)
32
+ end
33
+ else
34
+ instrument :download, key: key do
35
+ handle_errors do
36
+ _, io = blobs.get_blob(container, key)
37
+ io.force_encoding(Encoding::BINARY)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def download_chunk(key, range)
44
+ instrument :download_chunk, key: key, range: range do
45
+ handle_errors do
46
+ _, io = blobs.get_blob(container, key, start_range: range.begin, end_range: range.exclude_end? ? range.end - 1 : range.end)
47
+ io.force_encoding(Encoding::BINARY)
48
+ end
49
+ end
50
+ end
51
+
52
+ def delete(key)
53
+ instrument :delete, key: key do
54
+ blobs.delete_blob(container, key)
55
+ rescue Azure::Core::Http::HTTPError => e
56
+ raise unless e.type == "BlobNotFound"
57
+ # Ignore files already deleted
58
+ end
59
+ end
60
+
61
+ def delete_prefixed(prefix)
62
+ instrument :delete_prefixed, prefix: prefix do
63
+ marker = nil
64
+
65
+ loop do
66
+ results = blobs.list_blobs(container, prefix: prefix, marker: marker)
67
+
68
+ results.each do |blob|
69
+ blobs.delete_blob(container, blob.name)
70
+ end
71
+
72
+ break unless marker = results.continuation_token.presence
73
+ end
74
+ end
75
+ end
76
+
77
+ def exist?(key)
78
+ instrument :exist, key: key do |payload|
79
+ answer = blob_for(key).present?
80
+ payload[:exist] = answer
81
+ answer
82
+ end
83
+ end
84
+
85
+ def url(key, expires_in:, filename:, disposition:, content_type:)
86
+ instrument :url, key: key do |payload|
87
+ generated_url = signer.signed_uri(
88
+ uri_for(key), false,
89
+ service: "b",
90
+ permissions: "r",
91
+ expiry: format_expiry(expires_in),
92
+ content_disposition: content_disposition_with(type: disposition, filename: filename),
93
+ content_type: content_type
94
+ ).to_s
95
+
96
+ payload[:url] = generated_url
97
+
98
+ generated_url
99
+ end
100
+ end
101
+
102
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
103
+ instrument :url, key: key do |payload|
104
+ generated_url = signer.signed_uri(
105
+ uri_for(key), false,
106
+ service: "b",
107
+ permissions: "rw",
108
+ expiry: format_expiry(expires_in)
109
+ ).to_s
110
+
111
+ payload[:url] = generated_url
112
+
113
+ generated_url
114
+ end
115
+ end
116
+
117
+ def headers_for_direct_upload(key, content_type:, checksum:, **)
118
+ { "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-type" => "BlockBlob" }
119
+ end
120
+
121
+ private
122
+ def uri_for(key)
123
+ blobs.generate_uri("#{container}/#{key}")
124
+ end
125
+
126
+ def blob_for(key)
127
+ blobs.get_blob_properties(container, key)
128
+ rescue Azure::Core::Http::HTTPError
129
+ false
130
+ end
131
+
132
+ def format_expiry(expires_in)
133
+ expires_in ? Time.now.utc.advance(seconds: expires_in).iso8601 : nil
134
+ end
135
+
136
+ # Reads the object for the given key in chunks, yielding each to the block.
137
+ def stream(key)
138
+ blob = blob_for(key)
139
+
140
+ chunk_size = 5.megabytes
141
+ offset = 0
142
+
143
+ raise ActiveStorage::FileNotFoundError unless blob.present?
144
+
145
+ while offset < blob.properties[:content_length]
146
+ _, chunk = blobs.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1)
147
+ yield chunk.force_encoding(Encoding::BINARY)
148
+ offset += chunk_size
149
+ end
150
+ end
151
+
152
+ def handle_errors
153
+ yield
154
+ rescue Azure::Core::Http::HTTPError => e
155
+ case e.type
156
+ when "BlobNotFound"
157
+ raise ActiveStorage::FileNotFoundError
158
+ when "Md5Mismatch"
159
+ raise ActiveStorage::IntegrityError
160
+ else
161
+ raise
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class Service::Configurator #:nodoc:
5
+ attr_reader :configurations
6
+
7
+ def self.build(service_name, configurations)
8
+ new(configurations).build(service_name)
9
+ end
10
+
11
+ def initialize(configurations)
12
+ @configurations = configurations.deep_symbolize_keys
13
+ end
14
+
15
+ def build(service_name)
16
+ config = config_for(service_name.to_sym)
17
+ resolve(config.fetch(:service)).build(**config, configurator: self)
18
+ end
19
+
20
+ private
21
+ def config_for(name)
22
+ configurations.fetch name do
23
+ raise "Missing configuration for the #{name.inspect} Active Storage service. Configurations available for #{configurations.keys.inspect}"
24
+ end
25
+ end
26
+
27
+ def resolve(class_name)
28
+ require "active_storage/service/#{class_name.to_s.underscore}_service"
29
+ ActiveStorage::Service.const_get(:"#{class_name.camelize}Service")
30
+ rescue LoadError
31
+ raise "Missing service adapter for #{class_name.inspect}"
32
+ end
33
+ end
34
+ end