Skip to content

Instantly share code, notes, and snippets.

@julik
Created June 1, 2019 19:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save julik/4195b74774dc21dcd2cbd694a25102ce to your computer and use it in GitHub Desktop.
Save julik/4195b74774dc21dcd2cbd694a25102ce to your computer and use it in GitHub Desktop.
# Fixes for two ActiveStorage bugs which, separately and especially
# in combination, can lead to data being overwritten
module BlobKeyFixes
# How many times we should try to regenerate the key when a collision is encountered
TRIES_ON_NON_UNIQUE_KEY = 5
# Overridden to actually save the blob before attempting to upload to it,
# which makes the uniqueness constraint fire earlier if it gets triggered.
# Is a fix for https://github.com/rails/rails/issues/34805
def build_after_upload(io:, filename:, content_type: nil, metadata: nil)
new.tap do |blob|
blob.filename = filename
blob.content_type = content_type
blob.metadata = metadata
blob.byte_size = 0 # Default value to get past constraint
blob.checksum = "pending" # Default value to get past constraint
tries_remaining = TRIES_ON_NON_UNIQUE_KEY
begin
blob.save! # Will raise a uniqueness error if the constraint is hit
rescue ActiveRecord::RecordNotUnique
tries_remaining -= 1
raise if tries_remaining.zero?
blob.key = blob.class.generate_unique_secure_token
retry
end
blob.upload(io)
end
end
# Overridden to generate a base36 lowercase token instead of
# a base58 case-sensitive one.
# Is a fix for https://github.com/rails/rails/issues/34804
def generate_unique_secure_token
# With base58 used in Rails by default, we get
# [6] pry(main)> (58**24)
# => 210025412090735248552623050583059191142809
# possible values. The closest power to this with base36
# offering "no less than" possible values is 28:
# [14] pry(main)> (36**28)
# => 37711171281396032013366321198900157303750656
SecureRandom.base58(28).downcase
end
end
# In the initializer the ActiveStorage::Blob model is not yet available,
# so delay our monkeypatch until it becomes available
Rails.application.config.after_initialize do
Rails.logger.info { "ActiveStorage blob key override installed" }
# We need to prepend because `build_after_upload` is defined on
# the Blob class itself
class << ::ActiveStorage::Blob
prepend BlobKeyFixes
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment