Get default_url as file object

Hi,

we use shrine to mostly handle “private” uploads. We never serve files directly by using the “_url” helpers, we only serve them through controllers because we have to authorize the user before loading files.

For example:

class SomeController < ApplicationController
    def show
        authorize current_user
        user = User.find params[:id]
        send_file user.profile_picture.open
    end
end

This works great if the file exists. But if a user does not have a profile picture we want to send a default image. We have defined the default_url in the uploader, but as far as i can tell this only works for the “_url” helpers. “user.profile_picture” will return nil, but “user.profile_picture_url” generates a URL.

What we need is something like “user.profile_picture.open” but for the default_url. Is there a way to solve this with shrine right now?

Aha. Well, I don’t know if you could hook into whatever mechanism Shrine is giving you to resolve to the default image, but you could easily extend your controller method to do the same thing:

send_file user_picture
...
private

def user_picture
  return user.profile_picture.open if user.profile_picture

  Rails.root.join('path', 'to', 'default.jpg').open
end

Walter

Yeah, some time ago I was thinking of generalizing the “default URL” functionality for the whole attachment, but I was never sure if there are real use cases, thus I never allocated time for it. Walter’s suggestion is another alternative.

However, in your case, wouldn’t you need that behaviour only in this one controller action? In this case you could do:

send_file user.profile_picture.&open || "/path/to/default/image.jpg"

Is there something that makes this approach inconvenient?

Thank you for the quick response.

In my opinion, one of the best features of shrine is that i do not have to play around with file paths manually. Accessing the original file (user.profile_picture.open) or its derivatives (user.profile_picture_derivatiges[:thumb].open) is great, because i do not need to know what the actual filesystem looks like.

We basically followed your proposal (works as expected). I was just wondering if there is a “cleaner” way, without having to specify filepaths.

Thanks!

Hi all

was curious: if your profile_picture happened to be a very large file: perhaps 5TB, would using using an approach like: send_file user.profile_picture.open have performance implications for your rails/rack app? i.e. would it hold up the app preventing other requests from being met while the entire file is streamed to the requesting user?

Yes. The Ruby process will be fully involved the entire time that the file is streaming.

You probably want to redirect to the actual file endpoint in storage instead of the stream and send_file approach. If you have S3 or similar storage, you can calculate a presigned url and redirect to it, that way you have the security of routing through Rails, and the performance of streaming a file out of band.

Walter

1 Like

Just for the record, i wrote a small custom plugin to support the functionality that we need.
It is not perfect or feature complete but it solves our usecase. Some drawbacks:

  • The fallback will propably be a File object, while the (not missing) files normally are of type Shrine::UploadedFile
  • You cannot use square brackets to acces derivatives, it only works with function calls. so user.profile_picture and user.profile_picture(:small) work, but not user.profile_picture[:small] or user.profile_picture_derivatives[:small]

Code:

# Add the following line to your uploader:
#
#   plugin :default_file
#
# Then define +default_file+ method and return a file object, for example:
#
#   Attacher.default_file do |derivative|
#     version_name = derivative.presence || 'high'
#     File.open "app/assets/default_#{version_name}.png"
#   end
#
#
# Now when you access files always use the functions:
#   user.profile_picture
#   user.profile_picture(:small)
#
# You cannot use square brackets to load defaults (like user.profile_picture_derivatives[:small])
#
class Shrine
  module Plugins
    module DefaultFile
      def self.load_dependencies(uploader, **opts)
        uploader.plugin :derivatives
      end

      def self.configure(uploader, **opts)
        uploader.opts[:default_file] ||= {}
        uploader.opts[:default_file].merge!(opts)
      end

      module AttacherClassMethods
        def default_file(&block)
          shrine_class.opts[:default_file][:block] = block
        end
      end

      module AttacherMethods
        def get(*path)
          super(*path) || default_file(*path)
        end


        private

        def default_file(*options)
          return unless default_file_block

          instance_exec(*options, &default_file_block)
        end

        def default_file_block
          shrine_class.opts[:default_file][:block]
        end
      end
    end

    register_plugin(:default_file, DefaultFile)
  end
end