Skip to content

Instantly share code, notes, and snippets.

@kieranklaassen
Last active December 10, 2024 21:54
Show Gist options
  • Save kieranklaassen/b11183b6df88b169cbc19c72271ac360 to your computer and use it in GitHub Desktop.
Save kieranklaassen/b11183b6df88b169cbc19c72271ac360 to your computer and use it in GitHub Desktop.
active_storage_from_url.rb
# config/initializers/active_storage_from_url.rb
# frozen_string_literal: true
require "open-uri"
require "securerandom"
require "marcel"
# This initializer extends Active Storage attachments to support attaching files directly from a remote URL.
#
# Example Usage:
#
# class User < ApplicationRecord
# has_one_attached :avatar
# end
#
# user = User.find(params[:id])
# user.avatar.attach_from_url("https://example.com/image.jpg")
#
# class Gallery < ApplicationRecord
# has_many_attached :photos
# end
#
# gallery = Gallery.find(params[:id])
# gallery.photos.attach_from_url("https://example.com/another_image.jpg")
#
module ActiveStorage
class Attached
# Common logic for attaching files from remote URLs.
module URLAttachment # :nodoc:
private
def attach_from_url_impl(url)
return if url.blank?
uri = validate_remote_url!(url)
downloaded_file = nil
begin
downloaded_file = URI.open(uri)
filename = extract_filename(uri) || default_filename
content_type = determine_content_type(downloaded_file)
attach(io: downloaded_file, filename: filename, content_type: content_type)
ensure
cleanup_file(downloaded_file)
end
end
def validate_remote_url!(url)
uri = URI.parse(url)
unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
raise URI::InvalidURIError, "Invalid URL scheme: #{uri.scheme.inspect}"
end
uri
end
def extract_filename(uri)
path = uri.path
File.basename(path) if path.present? && path != "/"
end
def default_filename
"image_#{SecureRandom.uuid}.jpg"
end
def determine_content_type(file)
if file.respond_to?(:content_type) && file.content_type.present?
file.content_type
else
Marcel::MimeType.for(
file,
name: file.respond_to?(:path) ? File.basename(file.path) : nil
)
end
end
def cleanup_file(file)
return unless file
if file.respond_to?(:close!)
file.close!
else
file.close if file.respond_to?(:close)
file.unlink if file.respond_to?(:unlink)
end
end
end
# Reopen the One class to add attach_from_url.
class One
include URLAttachment
# Attaches a file from a remote URL to a has_one_attached association.
#
# @param url [String] The URL of the remote file.
# @raise [URI::InvalidURIError] If the URL is invalid or uses an unsupported scheme.
# @return [void]
def attach_from_url(url)
attach_from_url_impl(url)
end
end
# Reopen the Many class to add attach_from_url.
class Many
include URLAttachment
# Attaches a file from a remote URL to a has_many_attached association.
#
# @param url [String] The URL of the remote file.
# @raise [URI::InvalidURIError] If the URL is invalid or uses an unsupported scheme.
# @return [void]
def attach_from_url(url)
attach_from_url_impl(url)
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment