rails icon indicating copy to clipboard operation
rails copied to clipboard

support more cloud storage: direct upload with form data, customize HTTP method and response type

Open xiaohui-zhangxh opened this issue 1 year ago • 0 comments

Summary

When building a new Service for ActiveStorage, we got two requirements:

  1. We need to direct upload a file with POST method instead of PUT, and send credential(token), key(filename) and file(binary to upload) with Content-Type: multipart/form-data; boundary=<frontier>, instead of sending them with headers.
  2. We are creating a SaaS that make each Tenant has their own Cloud Service configuration, hence it's not possible to write all service configurations into config/storage.yml. For now, ActiveStorage::Blob has an anonymous validator to check service_name has to be declared in ActiveStorage::Blob.services, we can't disable this validator.

So, I think it's better to make them configurable like this:

# write my own service
module ActiveStorage
  class Service::QiniumService < Service

    # declare direct upload with HTTP POST method
    def http_method_for_direct_upload
      'POST'
    end

   # declare direct upload response type, "text" or "json"
    def http_response_type_for_direct_upload
      'json'
    end

    # declare direct upload file as multipart/form-data, the value of ':file' is the form data key to file
    def form_data_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, **)
      put_policy = Qinium::PutPolicy.new(config, key: key, expires_in: expires_in)
      put_policy.fsize_limit = content_length.to_i + 1000
      put_policy.mime_limit = content_type
      put_policy.detect_mime = 1
      put_policy.insert_only = 1
      {
        key: key,
        token: put_policy.to_token,
        ':file': 'file'
      }
    end
end

With above changes, ActiveStorage::Service can support all types of HTTP methods, send token to cloud service with HTTP header or form data.

change activestorage/app/models/active_storage/blob.rb anonymous validator to named one:

  validate do
    if service_name_changed? && service_name.present?
      services.fetch(service_name) do
        errors.add(:service_name, :invalid)
      end
    end
  end

changes to

validate :validate_service_name_in_services, if: -> { service_name_changed? && service_name.present? }
private
    def validate_service_name_in_services
      services.fetch(service_name) do
        errors.add(:service_name, :invalid)
      end
    end

Now, we can write our own Module prepend to ActiveStorage::Blob, to override validate_service_name_in_services, such as:

module ActiveStorageSaas::BlobModelMixin
  private
    def validate_service_name_in_services
      # dynamically define service name per TenantStorageService#id, later we can resolve tenant storage
      #   configuration by parsing TenantStorageService:1 to tenant_storage = TenantStorageService.find(1)
      #   tenant_storage.service_name => Real Storage Service Name
      #.  tenant_storage.service_options => options to make instance of Service
      /^TenantStorageService:\d+$/.match?(service_name) || super
    end
end

ActiveSupport.on_load(:active_storage_blob) do
  prepend ActiveStorageSaas::BlobModelMixin
end

Other Information

  • This is another PR #45442 that has the same requirement to customize HTTP Method
  • This is my real project that is working on this PR : https://github.com/xiaohui-zhangxh/activestorage_qinium/blob/main/lib/active_storage/service/qinium_service.rb
  • This is my real project that need to make our Tenants have their own storage configurations : https://github.com/xiaohui-zhangxh/activestorage_saas/blob/main/lib/active_storage/service/saas_service.rb

Now, I'm not ready to write test code before this idea of PR is accepted. Once this idea is accepted, I'd like to write some tests for it.

xiaohui-zhangxh avatar Aug 17 '22 07:08 xiaohui-zhangxh