activestorage_legacy 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. checksums.yaml +7 -0
  2. data/.babelrc +5 -0
  3. data/.codeclimate.yml +7 -0
  4. data/.eslintrc +19 -0
  5. data/.github/workflows/gem-push.yml +29 -0
  6. data/.github/workflows/ruby-tests.yml +37 -0
  7. data/.gitignore +9 -0
  8. data/.rubocop.yml +125 -0
  9. data/.travis.yml +25 -0
  10. data/Gemfile +33 -0
  11. data/Gemfile.lock +271 -0
  12. data/MIT-LICENSE +20 -0
  13. data/README.md +160 -0
  14. data/Rakefile +12 -0
  15. data/activestorage.gemspec +27 -0
  16. data/app/assets/javascripts/activestorage.js +1 -0
  17. data/app/controllers/active_storage/blobs_controller.rb +22 -0
  18. data/app/controllers/active_storage/direct_uploads_controller.rb +21 -0
  19. data/app/controllers/active_storage/disk_controller.rb +52 -0
  20. data/app/controllers/active_storage/variants_controller.rb +28 -0
  21. data/app/helpers/active_storage/file_field_with_direct_upload_helper.rb +18 -0
  22. data/app/javascript/activestorage/blob_record.js +54 -0
  23. data/app/javascript/activestorage/blob_upload.js +34 -0
  24. data/app/javascript/activestorage/direct_upload.js +42 -0
  25. data/app/javascript/activestorage/direct_upload_controller.js +67 -0
  26. data/app/javascript/activestorage/direct_uploads_controller.js +50 -0
  27. data/app/javascript/activestorage/file_checksum.js +53 -0
  28. data/app/javascript/activestorage/helpers.js +42 -0
  29. data/app/javascript/activestorage/index.js +11 -0
  30. data/app/javascript/activestorage/ujs.js +74 -0
  31. data/app/jobs/active_storage/purge_attachment_worker.rb +9 -0
  32. data/app/jobs/active_storage/purge_blob_worker.rb +9 -0
  33. data/app/models/active_storage/attachment.rb +33 -0
  34. data/app/models/active_storage/blob.rb +198 -0
  35. data/app/models/active_storage/filename.rb +49 -0
  36. data/app/models/active_storage/variant.rb +82 -0
  37. data/app/models/active_storage/variation.rb +53 -0
  38. data/config/routes.rb +9 -0
  39. data/config/storage_services.yml +34 -0
  40. data/lib/active_storage/attached/macros.rb +86 -0
  41. data/lib/active_storage/attached/many.rb +51 -0
  42. data/lib/active_storage/attached/one.rb +56 -0
  43. data/lib/active_storage/attached.rb +38 -0
  44. data/lib/active_storage/engine.rb +81 -0
  45. data/lib/active_storage/gem_version.rb +15 -0
  46. data/lib/active_storage/log_subscriber.rb +48 -0
  47. data/lib/active_storage/messages_metadata.rb +64 -0
  48. data/lib/active_storage/migration.rb +27 -0
  49. data/lib/active_storage/patches/active_record.rb +19 -0
  50. data/lib/active_storage/patches/delegation.rb +98 -0
  51. data/lib/active_storage/patches/secure_random.rb +26 -0
  52. data/lib/active_storage/patches.rb +4 -0
  53. data/lib/active_storage/service/azure_service.rb +115 -0
  54. data/lib/active_storage/service/configurator.rb +28 -0
  55. data/lib/active_storage/service/disk_service.rb +124 -0
  56. data/lib/active_storage/service/gcs_service.rb +79 -0
  57. data/lib/active_storage/service/mirror_service.rb +46 -0
  58. data/lib/active_storage/service/s3_service.rb +96 -0
  59. data/lib/active_storage/service.rb +113 -0
  60. data/lib/active_storage/verifier.rb +113 -0
  61. data/lib/active_storage/version.rb +8 -0
  62. data/lib/active_storage.rb +34 -0
  63. data/lib/tasks/activestorage.rake +20 -0
  64. data/package.json +33 -0
  65. data/test/controllers/direct_uploads_controller_test.rb +123 -0
  66. data/test/controllers/disk_controller_test.rb +57 -0
  67. data/test/controllers/variants_controller_test.rb +21 -0
  68. data/test/database/create_users_migration.rb +7 -0
  69. data/test/database/setup.rb +6 -0
  70. data/test/dummy/Rakefile +3 -0
  71. data/test/dummy/app/assets/config/manifest.js +5 -0
  72. data/test/dummy/app/assets/images/.keep +0 -0
  73. data/test/dummy/app/assets/javascripts/application.js +13 -0
  74. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  75. data/test/dummy/app/controllers/application_controller.rb +3 -0
  76. data/test/dummy/app/controllers/concerns/.keep +0 -0
  77. data/test/dummy/app/helpers/application_helper.rb +2 -0
  78. data/test/dummy/app/jobs/application_job.rb +2 -0
  79. data/test/dummy/app/models/application_record.rb +3 -0
  80. data/test/dummy/app/models/concerns/.keep +0 -0
  81. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  82. data/test/dummy/bin/bundle +3 -0
  83. data/test/dummy/bin/rails +4 -0
  84. data/test/dummy/bin/rake +4 -0
  85. data/test/dummy/bin/yarn +11 -0
  86. data/test/dummy/config/application.rb +22 -0
  87. data/test/dummy/config/boot.rb +5 -0
  88. data/test/dummy/config/database.yml +25 -0
  89. data/test/dummy/config/environment.rb +5 -0
  90. data/test/dummy/config/environments/development.rb +49 -0
  91. data/test/dummy/config/environments/production.rb +82 -0
  92. data/test/dummy/config/environments/test.rb +33 -0
  93. data/test/dummy/config/initializers/application_controller_renderer.rb +6 -0
  94. data/test/dummy/config/initializers/assets.rb +14 -0
  95. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  96. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  97. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  98. data/test/dummy/config/initializers/inflections.rb +16 -0
  99. data/test/dummy/config/initializers/mime_types.rb +4 -0
  100. data/test/dummy/config/initializers/secret_key.rb +3 -0
  101. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  102. data/test/dummy/config/routes.rb +2 -0
  103. data/test/dummy/config/secrets.yml +32 -0
  104. data/test/dummy/config/spring.rb +6 -0
  105. data/test/dummy/config/storage_services.yml +3 -0
  106. data/test/dummy/config.ru +5 -0
  107. data/test/dummy/db/.keep +0 -0
  108. data/test/dummy/lib/assets/.keep +0 -0
  109. data/test/dummy/log/.keep +0 -0
  110. data/test/dummy/package.json +5 -0
  111. data/test/dummy/public/404.html +67 -0
  112. data/test/dummy/public/422.html +67 -0
  113. data/test/dummy/public/500.html +66 -0
  114. data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
  115. data/test/dummy/public/apple-touch-icon.png +0 -0
  116. data/test/dummy/public/favicon.ico +0 -0
  117. data/test/filename_test.rb +36 -0
  118. data/test/fixtures/files/racecar.jpg +0 -0
  119. data/test/models/attachments_test.rb +122 -0
  120. data/test/models/blob_test.rb +47 -0
  121. data/test/models/variant_test.rb +27 -0
  122. data/test/service/.gitignore +1 -0
  123. data/test/service/azure_service_test.rb +14 -0
  124. data/test/service/configurations-example.yml +31 -0
  125. data/test/service/configurator_test.rb +14 -0
  126. data/test/service/disk_service_test.rb +12 -0
  127. data/test/service/gcs_service_test.rb +42 -0
  128. data/test/service/mirror_service_test.rb +62 -0
  129. data/test/service/s3_service_test.rb +52 -0
  130. data/test/service/shared_service_tests.rb +66 -0
  131. data/test/sidekiq/minitest_support.rb +6 -0
  132. data/test/support/assertions.rb +20 -0
  133. data/test/test_helper.rb +69 -0
  134. data/webpack.config.js +27 -0
  135. data/yarn.lock +3164 -0
  136. metadata +330 -0
@@ -0,0 +1,86 @@
1
+ # Provides the class-level DSL for declaring that an Active Record model has attached blobs.
2
+ module ActiveStorage::Attached::Macros
3
+ # Specifies the relation between a single attachment and the model.
4
+ #
5
+ # class User < ActiveRecord::Base
6
+ # has_one_attached :avatar
7
+ # end
8
+ #
9
+ # There is no column defined on the model side, Active Storage takes
10
+ # care of the mapping between your records and the attachment.
11
+ #
12
+ # Under the covers, this relationship is implemented as a `has_one` association to a
13
+ # `ActiveStorage::Attachment` record and a `has_one-through` association to a
14
+ # `ActiveStorage::Blob` record. These associations are available as `avatar_attachment`
15
+ # and `avatar_blob`. But you shouldn't need to work with these associations directly in
16
+ # most circumstances.
17
+ #
18
+ # The system has been designed to having you go through the `ActiveStorage::Attached::One`
19
+ # proxy that provides the dynamic proxy to the associations and factory methods, like `#attach`.
20
+ #
21
+ # If the +:dependent+ option isn't set, the attachment will be purged
22
+ # (i.e. destroyed) whenever the record is destroyed.
23
+ def has_one_attached(name, dependent: :purge_later)
24
+ define_method(name) do
25
+ instance_variable_get("@active_storage_attached_#{name}") ||
26
+ instance_variable_set("@active_storage_attached_#{name}", ActiveStorage::Attached::One.new(name, self))
27
+ end
28
+
29
+ if Rails.version < '4.0'
30
+ has_one :"#{name}_attachment", conditions: proc { "name = '#{name}'" }, class_name: "ActiveStorage::Attachment", as: :record
31
+ else
32
+ has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record
33
+ end
34
+
35
+ has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob
36
+
37
+ if dependent == :purge_later
38
+ before_destroy { public_send(name).purge_later }
39
+ end
40
+ end
41
+
42
+ # Specifies the relation between multiple attachments and the model.
43
+ #
44
+ # class Gallery < ActiveRecord::Base
45
+ # has_many_attached :photos
46
+ # end
47
+ #
48
+ # There are no columns defined on the model side, Active Storage takes
49
+ # care of the mapping between your records and the attachments.
50
+ #
51
+ # To avoid N+1 queries, you can include the attached blobs in your query like so:
52
+ #
53
+ # Gallery.where(user: Current.user).with_attached_photos
54
+ #
55
+ # Under the covers, this relationship is implemented as a `has_many` association to a
56
+ # `ActiveStorage::Attachment` record and a `has_many-through` association to a
57
+ # `ActiveStorage::Blob` record. These associations are available as `photos_attachments`
58
+ # and `photos_blobs`. But you shouldn't need to work with these associations directly in
59
+ # most circumstances.
60
+ #
61
+ # The system has been designed to having you go through the `ActiveStorage::Attached::Many`
62
+ # proxy that provides the dynamic proxy to the associations and factory methods, like `#attach`.
63
+ #
64
+ # If the +:dependent+ option isn't set, all the attachments will be purged
65
+ # (i.e. destroyed) whenever the record is destroyed.
66
+ def has_many_attached(name, dependent: :purge_later)
67
+ define_method(name) do
68
+ instance_variable_get("@active_storage_attached_#{name}") ||
69
+ instance_variable_set("@active_storage_attached_#{name}", ActiveStorage::Attached::Many.new(name, self))
70
+ end
71
+
72
+ if Rails.version < '4.0'
73
+ has_many :"#{name}_attachments", conditions: proc { "name = '#{name}'" }, as: :record, class_name: "ActiveStorage::Attachment"
74
+ else
75
+ has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment"
76
+ end
77
+
78
+ has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob
79
+
80
+ scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) }
81
+
82
+ if dependent == :purge_later
83
+ before_destroy { public_send(name).purge_later }
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,51 @@
1
+ # Decorated proxy object representing of multiple attachments to a model.
2
+ class ActiveStorage::Attached::Many < ActiveStorage::Attached
3
+ delegate_missing_to :attachments
4
+
5
+ # Returns all the associated attachment records.
6
+ #
7
+ # All methods called on this proxy object that aren't listed here will automatically be delegated to `attachments`.
8
+ def attachments
9
+ record.public_send("#{name}_attachments")
10
+ end
11
+
12
+ # Associates one or several attachments with the current record, saving them to the database.
13
+ # Examples:
14
+ #
15
+ # document.images.attach(params[:images]) # Array of ActionDispatch::Http::UploadedFile objects
16
+ # document.images.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
17
+ # document.images.attach(io: File.open("~/racecar.jpg"), filename: "racecar.jpg", content_type: "image/jpg")
18
+ # document.images.attach([ first_blob, second_blob ])
19
+ def attach(*attachables)
20
+ attachables.flatten.collect do |attachable|
21
+ attachments.create!(name: name, blob: create_blob_from(attachable))
22
+ end
23
+ end
24
+
25
+ # Returns true if any attachments has been made.
26
+ #
27
+ # class Gallery < ActiveRecord::Base
28
+ # has_many_attached :photos
29
+ # end
30
+ #
31
+ # Gallery.new.photos.attached? # => false
32
+ def attached?
33
+ attachments.any?
34
+ end
35
+
36
+ # Directly purges each associated attachment (i.e. destroys the blobs and
37
+ # attachments and deletes the files on the service).
38
+ def purge
39
+ if attached?
40
+ attachments.each(&:purge)
41
+ attachments.reload
42
+ end
43
+ end
44
+
45
+ # Purges each associated attachment through the queuing system.
46
+ def purge_later
47
+ if attached?
48
+ attachments.each(&:purge_later)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,56 @@
1
+ # Representation of a single attachment to a model.
2
+ class ActiveStorage::Attached::One < ActiveStorage::Attached
3
+ delegate_missing_to :attachment
4
+
5
+ # Returns the associated attachment record.
6
+ #
7
+ # You don't have to call this method to access the attachment's methods as
8
+ # they are all available at the model level.
9
+ def attachment
10
+ record.public_send("#{name}_attachment")
11
+ end
12
+
13
+ # Associates a given attachment with the current record, saving it to the database.
14
+ # Examples:
15
+ #
16
+ # person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object
17
+ # person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
18
+ # person.avatar.attach(io: File.open("~/face.jpg"), filename: "face.jpg", content_type: "image/jpg")
19
+ # person.avatar.attach(avatar_blob) # ActiveStorage::Blob object
20
+ def attach(attachable)
21
+ write_attachment \
22
+ ActiveStorage::Attachment.create!(record: record, name: name, blob: create_blob_from(attachable))
23
+ end
24
+
25
+ # Returns true if an attachment has been made.
26
+ #
27
+ # class User < ActiveRecord::Base
28
+ # has_one_attached :avatar
29
+ # end
30
+ #
31
+ # User.new.avatar.attached? # => false
32
+ def attached?
33
+ attachment.present?
34
+ end
35
+
36
+ # Directly purges the attachment (i.e. destroys the blob and
37
+ # attachment and deletes the file on the service).
38
+ def purge
39
+ if attached?
40
+ attachment.purge
41
+ write_attachment nil
42
+ end
43
+ end
44
+
45
+ # Purges the attachment through the queuing system.
46
+ def purge_later
47
+ if attached?
48
+ attachment.purge_later
49
+ end
50
+ end
51
+
52
+ private
53
+ def write_attachment(attachment)
54
+ record.public_send("#{name}_attachment=", attachment)
55
+ end
56
+ end
@@ -0,0 +1,38 @@
1
+ require "active_storage/blob"
2
+ require "active_storage/attachment"
3
+
4
+ require "action_dispatch/http/upload"
5
+ require "active_storage/patches/delegation"
6
+
7
+ # Abstract baseclass for the concrete `ActiveStorage::Attached::One` and `ActiveStorage::Attached::Many`
8
+ # classes that both provide proxy access to the blob association for a record.
9
+ class ActiveStorage::Attached
10
+ attr_reader :name, :record
11
+
12
+ def initialize(name, record)
13
+ @name, @record = name, record
14
+ end
15
+
16
+ private
17
+ def create_blob_from(attachable)
18
+ case attachable
19
+ when ActiveStorage::Blob
20
+ attachable
21
+ when ActionDispatch::Http::UploadedFile
22
+ ActiveStorage::Blob.create_after_upload! \
23
+ io: attachable.open,
24
+ filename: attachable.original_filename,
25
+ content_type: attachable.content_type
26
+ when Hash
27
+ ActiveStorage::Blob.create_after_upload!(attachable)
28
+ when String
29
+ ActiveStorage::Blob.find_signed(attachable)
30
+ else
31
+ nil
32
+ end
33
+ end
34
+ end
35
+
36
+ require "active_storage/attached/one"
37
+ require "active_storage/attached/many"
38
+ require "active_storage/attached/macros"
@@ -0,0 +1,81 @@
1
+ require "rails/engine"
2
+
3
+ module ActiveStorage
4
+ class Engine < Rails::Engine # :nodoc:
5
+ config.active_storage = ActiveSupport::OrderedOptions.new
6
+
7
+ # config.eager_load_namespaces << ActiveStorage
8
+
9
+ initializer "active_storage.logger" do
10
+ require "active_storage/service"
11
+
12
+ config.after_initialize do |app|
13
+ ActiveStorage::Service.logger = app.config.active_storage.logger || Rails.logger
14
+ end
15
+ end
16
+
17
+ initializer 'active_storage.extend_active_record' do
18
+ require "active_storage/patches"
19
+ require "active_storage/patches/active_record"
20
+
21
+ ActiveSupport.on_load :active_record do
22
+ extend ActiveStorage::Patches::ActiveRecord
23
+ end
24
+ end
25
+
26
+ initializer "active_storage.attached" do
27
+ require "active_storage/attached"
28
+
29
+ ActiveSupport.on_load(:active_record) do
30
+ extend ActiveStorage::Attached::Macros
31
+ end
32
+ end
33
+
34
+ # Port of Rails.application.key_generator.generate_key('ActiveStorage')
35
+ # Used to sign ActiveStorage::Blob
36
+ # See: https://github.com/rails/activestorage/blob/archive/lib/active_storage/engine.rb#L27
37
+ # https://github.com/rails/rails/blob/7-0-stable/activesupport/lib/active_support/key_generator.rb#L40
38
+ initializer "active_storage.verifier" do
39
+ require 'active_storage/verifier'
40
+
41
+ config.after_initialize do |app|
42
+ key = OpenSSL::PKCS5.pbkdf2_hmac(
43
+ app.config.secret_token,
44
+ 'ActiveStorage',
45
+ 1000,
46
+ 64,
47
+ OpenSSL::Digest::SHA256.new
48
+ )
49
+ ActiveStorage.verifier = ActiveStorage::Verifier.new(key)
50
+ end
51
+ end
52
+
53
+ initializer "active_storage.services" do
54
+ config.after_initialize do |app|
55
+ if config_choice = app.config.active_storage.service
56
+ config_file = Pathname.new(Rails.root.join("config/storage_services.yml"))
57
+ raise("Couldn't find Active Storage configuration in #{config_file}") unless config_file.exist?
58
+
59
+ require "yaml"
60
+ require "erb"
61
+
62
+ configs =
63
+ begin
64
+ YAML.load(ERB.new(config_file.read).result) || {}
65
+ rescue Psych::SyntaxError => e
66
+ raise "YAML syntax error occurred while parsing #{config_file}. " \
67
+ "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
68
+ "Error: #{e.message}"
69
+ end
70
+
71
+ ActiveStorage::Blob.service =
72
+ begin
73
+ ActiveStorage::Service.configure config_choice, configs
74
+ rescue => e
75
+ raise e, "Cannot load `Rails.config.active_storage.service`:\n#{e.message}", e.backtrace
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,15 @@
1
+ module ActiveStorage
2
+ # Returns the version of the currently loaded Active Storage as a <tt>Gem::Version</tt>
3
+ def self.gem_version
4
+ Gem::Version.new VERSION::STRING
5
+ end
6
+
7
+ module VERSION
8
+ MAJOR = 0
9
+ MINOR = 1
10
+ TINY = 0
11
+ PRE = "alpha"
12
+
13
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
14
+ end
15
+ end
@@ -0,0 +1,48 @@
1
+ require "active_support/log_subscriber"
2
+
3
+ class ActiveStorage::LogSubscriber < ActiveSupport::LogSubscriber
4
+ def service_upload(event)
5
+ message = "Uploaded file to key: #{key_in(event)}"
6
+ message << " (checksum: #{event.payload[:checksum]})" if event.payload[:checksum]
7
+ info event, color(message, GREEN)
8
+ end
9
+
10
+ def service_download(event)
11
+ info event, color("Downloaded file from key: #{key_in(event)}", BLUE)
12
+ end
13
+
14
+ def service_delete(event)
15
+ info event, color("Deleted file from key: #{key_in(event)}", RED)
16
+ end
17
+
18
+ def service_exist(event)
19
+ debug event, color("Checked if file exist at key: #{key_in(event)} (#{event.payload[:exist] ? "yes" : "no"})", BLUE)
20
+ end
21
+
22
+ def service_url(event)
23
+ debug event, color("Generated URL for file at key: #{key_in(event)} (#{event.payload[:url]})", BLUE)
24
+ end
25
+
26
+ def logger
27
+ ActiveStorage::Service.logger
28
+ end
29
+
30
+ private
31
+ def info(event, colored_message)
32
+ super log_prefix_for_service(event) + colored_message
33
+ end
34
+
35
+ def debug(event, colored_message)
36
+ super log_prefix_for_service(event) + colored_message
37
+ end
38
+
39
+ def log_prefix_for_service(event)
40
+ color " #{event.payload[:service]} Storage (#{event.duration.round(1)}ms) ", CYAN
41
+ end
42
+
43
+ def key_in(event)
44
+ event.payload[:key]
45
+ end
46
+ end
47
+
48
+ ActiveStorage::LogSubscriber.attach_to :active_storage
@@ -0,0 +1,64 @@
1
+ class ActiveSupport::MessagesMetadata #:nodoc:
2
+ def initialize(message, expires_at = nil, purpose = nil)
3
+ @message, @expires_at, @purpose = message, expires_at, purpose
4
+ end
5
+
6
+ def as_json(options = {})
7
+ { _rails: { message: @message, exp: @expires_at, pur: @purpose } }
8
+ end
9
+
10
+ class << self
11
+ def wrap(message, expires_at: nil, expires_in: nil, purpose: nil)
12
+ if expires_at || expires_in || purpose
13
+ ActiveSupport::JSON.encode new(encode(message), pick_expiry(expires_at, expires_in), purpose)
14
+ else
15
+ message
16
+ end
17
+ end
18
+
19
+ def verify(message, purpose)
20
+ extract_metadata(message).verify(purpose)
21
+ end
22
+
23
+ private
24
+ def pick_expiry(expires_at, expires_in)
25
+ if expires_at
26
+ expires_at.utc.iso8601(3)
27
+ elsif expires_in
28
+ Time.now.utc.advance(seconds: expires_in).iso8601(3)
29
+ end
30
+ end
31
+
32
+ def extract_metadata(message)
33
+ data = ActiveSupport::JSON.decode(message) rescue nil
34
+
35
+ if data.is_a?(Hash) && data.key?("_rails")
36
+ new(decode(data["_rails"]["message"]), data["_rails"]["exp"], data["_rails"]["pur"])
37
+ else
38
+ new(message)
39
+ end
40
+ end
41
+
42
+ def encode(message)
43
+ ::Base64.strict_encode64(message)
44
+ end
45
+
46
+ def decode(message)
47
+ ::Base64.strict_decode64(message)
48
+ end
49
+ end
50
+
51
+ def verify(purpose)
52
+ @message if match?(purpose) && fresh?
53
+ end
54
+
55
+ private
56
+ def match?(purpose)
57
+ @purpose.to_s == purpose.to_s
58
+ end
59
+
60
+ def fresh?
61
+ @expires_at.nil? || Time.now.utc < Time.iso8601(@expires_at)
62
+ end
63
+ end
64
+
@@ -0,0 +1,27 @@
1
+ class ActiveStorageCreateTables < ActiveRecord::Migration
2
+ def change
3
+ create_table :active_storage_blobs do |t|
4
+ t.string :key
5
+ t.string :filename
6
+ t.string :content_type
7
+ t.text :metadata
8
+ t.integer :byte_size
9
+ t.string :checksum
10
+ t.datetime :created_at
11
+ end
12
+
13
+ add_index :active_storage_blobs, :key, unique: true
14
+
15
+ create_table :active_storage_attachments do |t|
16
+ t.string :name
17
+ t.string :record_type
18
+ t.integer :record_id
19
+ t.integer :blob_id
20
+
21
+ t.datetime :created_at
22
+ end
23
+
24
+ add_index :active_storage_attachments, :blob_id
25
+ add_index :active_storage_attachments, [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ # Provides the class-level DSL for declaring that an Active Record model has attached blobs.
2
+
3
+ module ActiveStorage::Patches::ActiveRecord
4
+ class MinimumLengthError < StandardError; end
5
+ MINIMUM_TOKEN_LENGTH = 24
6
+
7
+ def has_secure_token(attribute = :token, length: MINIMUM_TOKEN_LENGTH)
8
+ if length < MINIMUM_TOKEN_LENGTH
9
+ raise MinimumLengthError, "Token requires a minimum length of #{MINIMUM_TOKEN_LENGTH} characters."
10
+ end
11
+
12
+ define_method("regenerate_#{attribute}") { update_attributes! attribute => self.class.generate_unique_secure_token(length: length) }
13
+ before_create { send("#{attribute}=", self.class.generate_unique_secure_token(length: length)) unless send("#{attribute}?") }
14
+ end
15
+
16
+ def generate_unique_secure_token(length: MINIMUM_TOKEN_LENGTH)
17
+ SecureRandom.base58(length)
18
+ end
19
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ class Module
6
+ # Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
7
+ # option is not used.
8
+ class DelegationError < NoMethodError; end
9
+
10
+ RUBY_RESERVED_KEYWORDS = %w(__ENCODING__ __LINE__ __FILE__ alias and BEGIN begin break
11
+ case class def defined? do else elsif END end ensure false for if in module next nil
12
+ not or redo rescue retry return self super then true undef unless until when while yield)
13
+ DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)
14
+ DELEGATION_RESERVED_METHOD_NAMES = Set.new(
15
+ RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
16
+ ).freeze
17
+
18
+ # When building decorators, a common pattern may emerge:
19
+ #
20
+ # class Partition
21
+ # def initialize(event)
22
+ # @event = event
23
+ # end
24
+ #
25
+ # def person
26
+ # detail.person || creator
27
+ # end
28
+ #
29
+ # private
30
+ # def respond_to_missing?(name, include_private = false)
31
+ # @event.respond_to?(name, include_private)
32
+ # end
33
+ #
34
+ # def method_missing(method, *args, &block)
35
+ # @event.send(method, *args, &block)
36
+ # end
37
+ # end
38
+ #
39
+ # With <tt>Module#delegate_missing_to</tt>, the above is condensed to:
40
+ #
41
+ # class Partition
42
+ # delegate_missing_to :@event
43
+ #
44
+ # def initialize(event)
45
+ # @event = event
46
+ # end
47
+ #
48
+ # def person
49
+ # detail.person || creator
50
+ # end
51
+ # end
52
+ #
53
+ # The target can be anything callable within the object, e.g. instance
54
+ # variables, methods, constants, etc.
55
+ #
56
+ # The delegated method must be public on the target, otherwise it will
57
+ # raise +DelegationError+. If you wish to instead return +nil+,
58
+ # use the <tt>:allow_nil</tt> option.
59
+ #
60
+ # The <tt>marshal_dump</tt> and <tt>_dump</tt> methods are exempt from
61
+ # delegation due to possible interference when calling
62
+ # <tt>Marshal.dump(object)</tt>, should the delegation target method
63
+ # of <tt>object</tt> add or remove instance variables.
64
+ def delegate_missing_to(target, allow_nil: nil)
65
+ target = target.to_s
66
+ target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)
67
+
68
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
69
+ def respond_to_missing?(name, include_private = false)
70
+ # It may look like an oversight, but we deliberately do not pass
71
+ # +include_private+, because they do not get delegated.
72
+
73
+ return false if name == :marshal_dump || name == :_dump
74
+ #{target}.respond_to?(name) || super
75
+ end
76
+
77
+ def method_missing(method, *args, &block)
78
+ if #{target}.respond_to?(method)
79
+ #{target}.public_send(method, *args, &block)
80
+ else
81
+ begin
82
+ super
83
+ rescue NoMethodError
84
+ if #{target}.nil?
85
+ if #{allow_nil == true}
86
+ nil
87
+ else
88
+ raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
89
+ end
90
+ else
91
+ raise
92
+ end
93
+ end
94
+ end
95
+ end
96
+ RUBY
97
+ end
98
+ end
@@ -0,0 +1,26 @@
1
+ require "securerandom"
2
+
3
+ module SecureRandom
4
+ BASE58_ALPHABET = ("0".."9").to_a + ("A".."Z").to_a + ("a".."z").to_a - ["0", "O", "I", "l"] unless defined?(BASE58_ALPHABET)
5
+ BASE36_ALPHABET = ("0".."9").to_a + ("a".."z").to_a unless defined?(BASE36_ALPHABET)
6
+
7
+ unless SecureRandom.methods.include?(:base58)
8
+ def self.base58(n = 16)
9
+ SecureRandom.random_bytes(n).unpack("C*").map do |byte|
10
+ idx = byte % 64
11
+ idx = SecureRandom.random_number(58) if idx >= 58
12
+ BASE58_ALPHABET[idx]
13
+ end.join
14
+ end
15
+ end
16
+
17
+ unless SecureRandom.methods.include?(:base36)
18
+ def self.base36(n = 16)
19
+ SecureRandom.random_bytes(n).unpack("C*").map do |byte|
20
+ idx = byte % 64
21
+ idx = SecureRandom.random_number(36) if idx >= 36
22
+ BASE36_ALPHABET[idx]
23
+ end.join
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,4 @@
1
+ module ActiveStorage::Patches
2
+ end
3
+
4
+ require "active_storage/patches/secure_random"