Skip to content

Instantly share code, notes, and snippets.

@bogdan
Created October 13, 2025 09:46
Show Gist options
  • Select an option

  • Save bogdan/c2fd6f01fa9b61cabf0e960e0272d293 to your computer and use it in GitHub Desktop.

Select an option

Save bogdan/c2fd6f01fa9b61cabf0e960e0272d293 to your computer and use it in GitHub Desktop.
require 'net/http'
require 'net/https'
require 'json'
require 'uri'
class ActiveStorage::Service::PinataService < ActiveStorage::Service
BASE_URL = "https://uploads.pinata.cloud"
class_attribute :files, default: {}, instance_writer: false
attr_reader :jwt, :gateway
def initialize(
gateway: "ipfs://",
api_key:,
api_secret:,
jwt:,
public:,
group_id: nil
)
@jwt = jwt
@gateway = gateway
@api_key = api_key
@api_secret = api_secret
@group_id = group_id
@public = public
end
def upload(key, io, **options)
instrument :upload, key: do
uploaded_cid = upload_to_pinata(key, io)
update_cid(key, uploaded_cid)
end
end
def update_cid(key, cid)
blob = ActiveStorage::Blob.find_by!(key:)
return if blob.custom_metadata[:cid] == cid
blob.custom_metadata = {**blob.custom_metadata, cid: cid}
blob.save!
end
def download(key, &block)
instrument :download, key: do
uri = URI(url(key))
if Rails.env.test?
cid = cid_by_key(key)
if content = self.files[cid]
if block
return block.call(content)
else
return StringIO.new(self.files[cid])
end
else
return IPFSUtils.fixture_file(cid, &block)
end
end
Errors.retryable(on: ActiveStorage::FileNotFoundError, interval: 3) do
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(Net::HTTP::Get.new(uri)) do |response|
unless response.is_a?(Net::HTTPSuccess)
raise ActiveStorage::FileNotFoundError, "File not found at #{uri}. Code: #{response.code.to_i}. Body: #{response.body}"
end
if block
response.read_body do |chunk|
block.call(chunk)
end
else
return StringIO.new(response.body)
end
end
end
end
end
end
def open(key, verify: true, **options, &block)
super(key, verify: verify && !Rails.env.test?, **options, &block)
end
def delete(key)
# Unpinning should not happen
# Because we guarantee files availability to our clients
# instrument :delete, key: do
# uri = URI("#{PINATA_BASE_URL}/pinning/unpin")
# request = Net::HTTP::Post.new(uri, headers)
# request.body = { ipfs_pin_hash: key }.to_json
# response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
# raise ActiveStorage::FileNotFoundError, "Delete failed: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
# end
end
def url(key, cid: nil, **options)
url = gateway_url(cid || key)
unless url
raise ActiveStorage::IntegrityError, "Ipfs cid is not specified for #{key.inspect}"
end
url
end
def exist?(key)
instrument :exists, key: do
url = gateway_url(key)
return false unless url
if Rails.env.test?
return !!self.class.files[IPFSUtils.normalize_cid(url)]
end
uri = URI(url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
response = http.head(uri.path)
response.is_a?(Net::HTTPSuccess)
end
end
def public?
@public
end
def network
public? ? 'public' : 'private'
end
private
def upload_to_pinata(key, io)
if Rails.env.test?
cid = IPFSUtils.calculate_cid_v1(io)
unless self.class.files[cid]
self.class.files[cid] = io.read
end
return cid
end
uri = URI("#{BASE_URL}/v3/files")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{jwt}"
form_data = [
["network", network],
["file", io],
]
# Optional metadata
form_data << ["group_id", @group_id] if @group_id
form_data << ["keyvalues", { key: }.to_json]
request.set_form(form_data, 'multipart/form-data')
Errors.retryable(on: [IOError, Net::OpenTimeout]) do
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
raise ActiveStorage::IntegrityError, "Upload failed: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
json = JSON.parse(response.body)
json.dig("data", "cid") || raise(ActiveStorage::IntegrityError, "Upload response missing CID: #{response.body}")
end
end
def gateway_url(value)
cid = cid_by_key(value)
return nil unless cid
unless IPFSUtils.valid?(cid)
raise ActiveStorage::IntegrityError, "Blob metadata CID #{cid.inspect} is invalid"
end
[@gateway, @gateway.end_with?('/') ? "" : "/", cid].join('')
end
def cid_by_key(key)
IPFSUtils.valid?(key) ? key : ActiveStorage::Blob.where(key:).pick(:metadata)&.fetch(:custom, nil)&.fetch(:cid, nil)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment