Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/parklife/rails/activestorage.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# frozen_string_literal: true
require_relative 'blob_modifications'

module Parklife
module Rails
module ActiveStorage
Expand Down
65 changes: 65 additions & 0 deletions lib/parklife/rails/blob_modifications.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true
module Parklife
module Rails
module BlobModifications
def self.prepended(base)
base.before_validation :set_parklife_key
end

# Generate a deterministic MD5 hash for the Blob based on its attributes.
#
# This means that for a completely fresh Parklife build the same file with
# the same contents will be stored with the same filename so there's no
# unnecessary cache-busting between freshly-built deployments.
#
# The hash is also guaranteed to be unique so it's possible that the same
# filename/contents creates a different value, in this case it's also
# produced deterministically so successive hashes will be the same - but
# ideally you should prevent this from happening in the first place.
#
# @return [String]
def generate_parklife_key
key = nil
i = 0

loop do
key = OpenSSL::Digest::MD5.hexdigest(
[
byte_size,
checksum,
content_type,
filename,
service_name,
i,
].map(&:to_s).join
)

break if self.class.where(key: key).none?

i += 1
end

key
end

# Override the standard setter so `has_secure_token :key` does nothing.
def key=(_)
nil
end

# Override the standard getter which otherwise assigns a random token.
def key
self[:key]
end

private
def set_parklife_key
self[:key] ||= generate_parklife_key
end
end
end
end

ActiveSupport.on_load(:active_storage_blob) do
ActiveStorage::Blob.prepend Parklife::Rails::BlobModifications
end
File renamed without changes.
88 changes: 88 additions & 0 deletions spec/parklife/blob_modifications_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
require 'rails_helper'

RSpec.describe Parklife::Rails::BlobModifications do
context 'when the same file/filename is attached' do
it 'the same key is assigned' do
post = FactoryBot.create(:post)
post.hero.attach(
filename: 'plasma.jpg',
io: file_fixture('plasma.jpg').open,
)

id1 = post.hero.blob.id
key1 = post.hero.blob.key

post.hero.purge
post.hero.attach(
filename: 'plasma.jpg',
io: file_fixture('plasma.jpg').open,
)

id2 = post.hero.blob.id
key2 = post.hero.blob.key

expect(id2).not_to eql(id1)
expect(key2).to eql(key1)
end
end

context 'when the same file with a different filename is attached' do
it 'a different key is assigned' do
post1 = FactoryBot.create(:post)
post1.hero.attach(
filename: 'foo.jpg',
io: file_fixture('plasma.jpg').open,
)

post2 = FactoryBot.create(:post)
post2.hero.attach(
filename: 'bar.jpg',
io: file_fixture('plasma.jpg').open,
)

key1 = post1.hero.blob.key
key2 = post2.hero.blob.key

expect(key1).not_to eql(key2)
end
end

context 'when a duplicate file/filename is attached' do
it 'a different key is assigned' do
post1 = FactoryBot.create(:post)
post1.hero.attach(
filename: 'plasma.jpg',
io: file_fixture('plasma.jpg').open,
)

post2 = FactoryBot.create(:post)
post2.hero.attach(
filename: 'plasma.jpg',
io: file_fixture('plasma.jpg').open,
)

key1 = post1.hero.blob.key
key2 = post2.hero.blob.key

expect(key1).not_to eql(key2)

post1.hero.purge
post2.hero.purge

post1.hero.attach(
filename: 'plasma.jpg',
io: file_fixture('plasma.jpg').open,
)
post2.hero.attach(
filename: 'plasma.jpg',
io: file_fixture('plasma.jpg').open,
)

key3 = post1.hero.blob.key
key4 = post2.hero.blob.key

expect(key3).to eql(key1)
expect(key4).to eql(key2)
end
end
end