Skip to content
Permalink
Browse files

Store newly-uploaded files on save rather than assignment

  • Loading branch information...
georgeclaghorn committed Jul 8, 2018
1 parent 0b534cd commit e8682c5bf051517b0b265e446aa1a7eccfd47bf7
@@ -1,3 +1,17 @@
* Uploaded files assigned to a record are persisted to storage when the record
is saved instead of immediately.

In Rails 5.2, the following causes an uploaded file in `params[:avatar]` to
be stored:

```ruby
@user.avatar = params[:avatar]
```

In Rails 6, the uploaded file is stored when `@user` is successfully saved.

*George Claghorn*

* Add the ability to reflect on defined attachments using the existing
ActiveRecord reflection mechanism.

@@ -15,17 +15,18 @@ class ActiveStorage::Attachment < ActiveRecord::Base
delegate_missing_to :blob

after_create_commit :analyze_blob_later, :identify_blob
after_destroy_commit :purge_dependent_blob_later

# Synchronously purges the blob (deletes it from the configured service) and destroys the attachment.
# Synchronously purges the blob (deletes it from the configured service) and deletes the attachment.
def purge
blob.purge
destroy
delete
end

# Destroys the attachment and asynchronously purges the blob (deletes it from the configured service).
# Deletes the attachment and queues a background job to purge the blob (delete it from the configured service).
def purge_later
blob.purge_later
destroy
delete
end

private
@@ -36,4 +37,13 @@ def identify_blob
def analyze_blob_later
blob.analyze_later unless blob.analyzed?
end

def purge_dependent_blob_later
blob.purge_later if dependent == :purge_later
end


def dependent
record.attachment_reflections[name]&.options[:dependent]
end
end
@@ -48,15 +48,17 @@ def find_signed(id)
# Returns a new, unsaved blob instance after the +io+ has been uploaded to the service.
# When providing a content type, pass <tt>identify: false</tt> to bypass automatic content type inference.
def build_after_upload(io:, filename:, content_type: nil, metadata: nil, identify: true)
new.tap do |blob|
blob.filename = filename
blob.content_type = content_type
blob.metadata = metadata

new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob|
blob.upload(io, identify: identify)
end
end

def build_after_unfurling(io:, filename:, content_type: nil, metadata: nil, identify: true) #:nodoc:
new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob|
blob.unfurl(io, identify: identify)
end
end

# Returns a saved blob instance after the +io+ has been uploaded to the service. Note, the blob is first built,
# then the +io+ is uploaded, then the blob is saved. This is done this way to avoid uploading (which may take
# time), while having an open database transaction.
@@ -152,12 +154,19 @@ def service_headers_for_direct_upload
# Normally, you do not have to call this method directly at all. Use the factory class methods of +build_after_upload+
# and +create_after_upload!+.
def upload(io, identify: true)
unfurl io, identify: identify
upload_without_unfurling io
end

def unfurl(io, identify: true) #:nodoc:
self.checksum = compute_checksum_in_chunks(io)
self.content_type = extract_content_type(io) if content_type.nil? || identify
self.byte_size = io.size
self.identified = true
end

service.upload(key, io, checksum: checksum)
def upload_without_unfurling(io) #:nodoc:
service.upload key, io, checksum: checksum
end

# Downloads the file associated with this blob. If no block is given, the entire file is read into memory and returned.
@@ -15,6 +15,10 @@ def initialize(name, record, dependent:)
end

private
def change
record.attachment_changes[name]
end

def create_blob_from(attachable)
case attachable
when ActiveStorage::Blob
@@ -35,6 +39,7 @@ def create_blob_from(attachable)
end
end

require "active_storage/attached/model"
require "active_storage/attached/one"
require "active_storage/attached/many"
require "active_storage/attached/macros"
require "active_storage/attached/changes"
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module ActiveStorage
module Attached::Changes #:nodoc:
extend ActiveSupport::Autoload

eager_autoload do
autoload :CreateOne
autoload :CreateMany
autoload :CreateOneOfMany

autoload :DeleteOne
autoload :DeleteMany
end
end
end
@@ -0,0 +1,32 @@
# frozen_string_literal: true

module ActiveStorage
class Attached::Changes::CreateMany #:nodoc:
attr_reader :name, :record, :attachables

def initialize(name, record, attachables)
@name, @record, @attachables = name, record, Array(attachables)
end

def attachments
@attachments ||= subchanges.collect(&:attachment)
end

def upload
subchanges.each(&:upload)
end

def save
record.public_send("#{name}_attachments=", attachments)
end

private
def subchanges
@subchanges ||= attachables.collect { |attachable| build_subchange_from(attachable) }
end

def build_subchange_from(attachable)
ActiveStorage::Attached::Changes::CreateOneOfMany.new(name, record, attachable)
end
end
end
@@ -0,0 +1,65 @@
# frozen_string_literal: true

module ActiveStorage
class Attached::Changes::CreateOne #:nodoc:
attr_reader :name, :record, :attachable

def initialize(name, record, attachable)
@name, @record, @attachable = name, record, attachable
end

def attachment
@attachment ||= find_or_build_attachment
end

def blob
@blob ||= find_or_build_blob
end

def upload
case attachable
when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
blob.upload_without_unfurling(attachable.open)
when Hash
blob.upload_without_unfurling(attachable.fetch(:io))
end
end

def save
record.public_send("#{name}_attachment=", attachment)
end

private
def find_or_build_attachment
find_attachment || build_attachment
end

def find_attachment
if record.public_send("#{name}_blob") == blob
record.public_send("#{name}_attachment")
end
end

def build_attachment
ActiveStorage::Attachment.new(record: record, name: name, blob: blob)
end

def find_or_build_blob
case attachable
when ActiveStorage::Blob
attachable
when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
ActiveStorage::Blob.build_after_unfurling \
io: attachable.open,
filename: attachable.original_filename,
content_type: attachable.content_type
when Hash
ActiveStorage::Blob.build_after_unfurling(attachable)
when String
ActiveStorage::Blob.find_signed(attachable)
else
raise "Could not find or build blob: expected attachable, got #{attachable.inspect}"
end
end
end
end
@@ -0,0 +1,10 @@
# frozen_string_literal: true

module ActiveStorage
class Attached::Changes::CreateOneOfMany < Attached::Changes::CreateOne #:nodoc:
private
def find_attachment
record.public_send("#{name}_attachments").detect { |attachment| attachment.blob_id == blob.id }
end
end
end
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module ActiveStorage
class Attached::Changes::DeleteMany #:nodoc:
attr_reader :name, :record

def initialize(name, record)
@name, @record = name, record
end

def attachments
ActiveStorage::Attachment.none
end

def save
record.public_send("#{name}_attachments=", [])
end
end
end
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module ActiveStorage
class Attached::Changes::DeleteOne #:nodoc:
attr_reader :name, :record

def initialize(name, record)
@name, @record = name, record
end

def attachment
nil
end

def save
record.public_send("#{name}_attachment=", nil)
end
end
end
Oops, something went wrong.

0 comments on commit e8682c5

Please sign in to comment.
You can’t perform that action at this time.