Shrine Documentation

repository·master·Indexed 23 days ago

https://github.com/shrinerb/shrine

A modular toolkit for handling file attachments in Ruby applications. Shrine supports various storage backends including S3, GCS, and FileSystem, and integrates with multiple ORMs such as ActiveRecord, Sequel, and Mongoid. It provides advanced features for direct client-side uploads via Uppy, background processing for derivatives, metadata validation, and flexible uploader configurations.

Tokens
97.4K
Snippets
383
Records
528
Agent score
84%

What's inside Shrine

  1. Understand design differences between Paperclip and Shrine

    master

    When transitioning from Paperclip to Shrine, note these fundamental architectural shifts:

    • Uploader Logic: Paperclip configures attachments directly in models. Shrine encapsulates logic in dedicated Uploader classes.
    • Storage: Paperclip couples storage configuration to the model. Shrine uses decoupled Shrine.storages objects and supports a concept of 'temporary' (cache) storage.
    • Persistence: Paperclip uses multiple columns (_file_name, _content_type, etc.). Shrine uses a single <name>_data column containing a JSON object with id, storage, and metadata.
    • Location: Paperclip stores only the filename and calculates paths dynamically. Shrine persists the full location in the JSON data, making it more resilient to configuration changes.
    • Processing: Paperclip uses model-level styles. Shrine uses instance-level processing (e.g., via the derivatives plugin), allowing for more control and agnostic processing tools (like libvips via image_processing).
  2. Understand Shrine core classes

    master

    Shrine distributes responsibilities across several specialized classes rather than using a single 'god object':

    ClassDescription
    Shrine::Storage::*Encapsulates file operations for the underlying storage service
    ShrineWraps uploads and handles loading plugins
    Shrine::UploadedFileRepresents a file that was uploaded to storage
    Shrine::AttacherHandles attaching files to records
    Shrine::AttachmentAdds convenience attachment methods to model instances
  3. Understand Shrine::Attacher and its relationship to Attachment

    master

    Attachment logic is managed by a Shrine::Attacher object. When you include Shrine::Attachment in a class, it provides a convenience layer that allows you to access the attacher via a <name>_attacher method.

    For example, if you include ImageUploader::Attachment(:image), you can access the attacher using photo.image_attacher.

    class Photo
      include ImageUploader::Attachment(:image)
    end
    
    photo = Photo.new
    photo.image_attacher #=> #<ImageUploader::Attacher>
  4. Understand Shrine's core architecture vs CarrierWave

    master

    Shrine distributes responsibilities that CarrierWave handles in a single uploader class across three core classes:

    • Shrine: Handles uploads, metadata extraction, and location generation.
    • Shrine::UploadedFile: Exposes metadata, implements downloading, URL generation, and deletion.
    • Shrine::Attacher: Handles caching & storing, dirty tracking, persistence, and versions.

    Unlike CarrierWave, Shrine uploaders are functional: they receive a file on input and return an UploadedFile on output without internal state changes.

    uploader      = ImageUploader.new(:store)
    uploaded_file = uploader.upload(file, :store)
    uploaded_file          #=> #<Shrine::UploadedFile>
    uploaded_file.url      #=> "https://my-bucket.s3.amazonaws.com/store/kfds0lg9rer.jpg"
    uploaded_file.download #=> #<File:/tmp/path/to/file>
  5. Use the included plugin to extend models with attachment methods

    master

    The included plugin allows you to hook into the .included lifecycle event that occurs when an attachment module is included into a model. This enables you to dynamically define methods on the model class based on the attachment name. The block receives the attachment name (e.g., :image) as an argument.

    class ImageUploader < Shrine
      plugin :included do |name|
        # self is the model class (e.g., Photo)
        # name is the attachment name (e.g., :image)
        define_method(:"#{name}_width") { send(name)&.width }
        define_method(:
    "#{name}_height") { send(name)&.height }
      end
    end
    
    class Photo
      include ImageUploader::Attachment(:image)
    end
    
    # Usage:
    photo = Photo.new(image: file)
    photo.image_width  #=> 1200
    photo.image_height #=> 800
  6. Replicate Refile form helpers in Shrine

    master

    Shrine does not have a direct attachment_field helper. Instead, use the cached_attachment_data plugin and manually render hidden fields for the cached data alongside a standard file field.

    Replace attachment_field

    Refile:

    form_for @user do |form|
      form.attachment_field :profile_image
    end

    Shrine:

    Shrine.plugin :cached_attachment_data
    
    form_for @user do |form|
      form.hidden_field :profile_image, value: @user.cached_profile_image_data, id: nil
      form.file_field :profile_image
    end

    Replace remove_<attachment>

    Use the remove_attachment plugin to add a removal method to your model.

    Refile: (Uses remove_<attachment> method)

    Shrine:

    Shrine.plugin :remove_attachment
    
    form_for @user do |form|
      form.hidden_field :profile_image, value: @user.cached_profile_image_data, id: nil
      form.file_field :profile_image
      form.check_box :remove_profile_image
    end

    Replace remote_<attachment>_url

    Use the remote_url plugin to add the #<attachment>_remote_url method to your model.

    Refile: (Uses remote_<attachment>_url method)

    Shrine:

    Shrine.plugin :remote_url
    
    form_for @user do |form|
      form.hidden_field :profile_image, value: @user.cached_profile_image_data, id: nil
      form.file_field :profile_image
      form.text_field :profile_image_remote_url
    end
    # Example of replacing attachment_field with Shrine
    Shrine.plugin :cached_attachment_data
    
    form_for @user do |form|
      form.hidden_field :profile_image, value: @user.cached_profile_image_data, id: nil
      form.file_field :profile_image
    end
  7. Configure backgrounding for promotion and destruction

    master

    To use backgrounding, you must define background jobs (e.g., using Sidekiq) and register them using Shrine::Attacher.promote_block and Shrine::Attacher.destroy_block.

    Global Configuration

    Register blocks globally in your initializer to apply to all uploaders:

    Shrine::Attacher.promote_block do
      PromoteJob.perform_async(self.class.name, record.class.name, record.id, name.to_s, file_data)
    end
    
    Shrine::Attacher.destroy_block do
      DestroyJob.perform_async(self.class.name, data)
    end

    Uploader-Specific Configuration

    Alternatively, you can define these blocks within a specific uploader class:

    class MyUploader < Shrine
      Attacher.promote_block do
        PromoteJob.perform_async(self.class.name, record.class.name, record.id, name.to_s, file_data)
      end
    
      Attacher.destroy_block do
        DestroyJob.perform_async(self.class.name, data)
      end
    end
    # Example of a PromoteJob implementation
    class PromoteJob
      include Sidekiq::Worker
    
      def perform(attacher_class, record_class, record_id, name, file_data)
        attacher_class = Object.const_get(attacher_class)
        record         = Object.const_get(record_class).find(record_id)
    
        attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
        attacher.atomic_promote
      rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
        # attachment has changed or record has been deleted, nothing to do
      end
    end
    
    # Example of a DestroyJob implementation
    class DestroyJob
      include Sidekiq::Worker
    
      def perform(attacher_class, data)
        attacher_class = Object.const_get(attacher_class)
    
        attacher = attacher_class.from_data(data)
        attacher.destroy
      end
    end
  8. Configure the multi_cache plugin

    master

    The multi_cache plugin enables an attacher to accept files from multiple temporary storages. To use it, first define your storages in Shrine.storages, then register the plugin and specify which additional storages are allowed to act as caches using the additional_cache option.

    Example configuration:

    Shrine.storages = { cache: ..., cache_one: ..., cache_two: ..., store: ... }
    
    Shrine.plugin :multi_cache, additional_cache: [:cache_one, :cache_two]
  9. Perform acceptance tests for file uploads

    master

    For end-to-end testing, use the following patterns depending on your testing tool:

    • Capybara: Use attach_file.
    • Rack-test: Post a Rack::Test::UploadedFile.
    • Cached Data: To test requests with cached data, upload the file to :cache first and pass its JSON representation.
    # Capybara
    attach_file("#image-field", "test/files/image.jpg")
    
    # rack-test
    post "/photos", photo: {
      image: Rack::Test::UploadedFile.new("test/files/image.jpg", "image/jpeg")
    }
    
    # Testing with cached attachment data
    cached_file = Shrine.upload(file, :cache)
    post "/photos", photo: { image: cached_file.to_json }