Shrine: Derivatives plugin – a better "versions" plugin

Created on 23 May 2019  ·  1Comment  ·  Source: shrinerb/shrine

In the past we've had a lot of discussions (and some attempts) regarding rewriting the versions plugin into something more flexible (1, 2, 3, 4, 5, 6).

Note that this issue isn't for discussion, it's more for tracking progress. We'll hopefully discuss and figure out details while making pull requests.

Problems with versions

The current versions plugin has couple of downsides and limitations:

1️⃣ Adding new versions after promotion. If you're allowing users to crop uploaded images after attaching, it's currently fairly difficult to add the cropped version to an existing attachment. The versions plugin only gives you the API for generating versions as part of attachment.

2️⃣ Data structure changing. The original file needs to be fetched differently depending on whether it's still cached (photo.image) or stored (photo.image[:original]). This brings inconsistency to using Shrine, as shown by some issues posted by users (e.g. https://github.com/shrinerb/shrine/issues/364, https://github.com/shrinerb/shrine/issues/305).

3️⃣ Different storage for different versions. It's common to want to store the original file in storage A, but versions in storage B. Currently this is somewhat possible, but it's hacky and most people won't figure that out.

Some related plugins are have also shown to be confusing/problematic in the past, and those could be deprecated if we rewrote versions:

  • versions
  • processing (maybe)
  • recache
  • delete_raw
  • parallelize (this one is ok, but now user can do their own parallelization)
  • parsed_json (this functionality would be merged into base)

Solution

The new plugin would be called derivatives (as these files would be _"derived"_ from a source). Given the following setup:

class ImageUploader < Shrine
  plugin :derivatives
  # or
  plugin :derivatives, storage: :thumbnails
end

# ...

class Photo < Sequel::Model
  include ImageUploader::Attachment.new(:image)
end

The API could look something like this:

add_derivative(s)

This method uploads given files to storage, assigns uploaded file data to the attachment column, and _deletes_ the source files.

photo.image_attacher.add_derivative(:small,  thumbnail_300)
photo.image_attacher.add_derivative(:medium, thumbnail_500)
photo.image_attacher.add_derivative(:large,  thumbnail_800)
# or
photo.image_attacher.add_derivatives(
  small:  thumbnail_300,
  medium: thumbnail_500,
  large:  thumbnail_800,
)

photo.image_data #=> '{
#   "id": "...",
#   "storage": "...",
#   "metadata": {...},
#   "derivatives": {
#     "small": { "id": "...", "storage": "...", "metadata": {...} },
#     "medium": { "id": "...", "storage": "...", "metadata": {...} },
#     "large": { "id": "...", "storage": "...", "metadata": {...} },
#   }
# }'

photo.image #=> #<Shrine::UploadedFile> (original)

photo.image_derivatives #=> {
#   small: #<Shrine::UploadedFile>,
#   medium: #<Shrine::UploadedFile>,
#   large: #<Shrine::UploadedFile>,
# }

This method can also receive Shrine::UploadedFile objects, in case the files have already been uploaded elsewhere. You can also customize the upload:

photo.image_attacher.add_derivative(:small, thumbnail_300, {
  storage: :other_store,
  location: "/path/to/file",
  metadata: { "filename" => "..." },
})

remove_derivative(s)

Does the opposite of add_derivative(s), removes the derivative from the column (and deletes the uploaded file).

Backends

By default, derivatives would be stored in the same attachment column as the original, under the derivatives field. However, if it would be too difficult to ensure there are no race conditions, I'm open for ditching this approach.

I also would like to support an alternative way of storing files, via blobs and attachments tables like Active Storage has, for original file and derivatives. That way people would only need to create tables once, they could query files more easily, and it would be easier to eliminate race conditions. This would be only possible after https://github.com/shrinerb/shrine/issues/381.

Considerations

This plan is by no means set in stone, there are still tons of things to consider when designing the API:

  • The API should be convenient enough to use. Currently I have the following, which might not be the enough:

    photo.image.download do |original|
     processor = ImageProcessing::Vips.source(original)
    
     photo.image_attacher.add_derivatives(
       small: processor.resize_to_limit!(300, 300),
       medium: processor.resize_to_limit!(500, 500),
       large: processor.resize_to_limit!(800, 800),
     )
    end
    
  • We need to make adding/removing derivatives concurrency-safe. This means it should be possible to add two different derivatives in two different parallel jobs, without any possibility of them overwriting each other when results are saved into the database column (we plan to use pessimistic locking). It should be also possible to automatically abort adding derivatives if the original attachment has changed (we'll need to adapt the backgrounding plugin).
  • The API should work for ORMs that implement the Active Record pattern (ActiveRecord, Sequel, Mongoid) and those that implement the Repository pattern (ROM, Hanami::Model).
  • We should support nested Hashes and Arrays of derivatives.
  • It would probably be nice to somehow be able to merge derivation declarations from the derivation_endpoint plugin, if people want to use both derivatives and derivation_endpoint plugins in tandem.
feature

Most helpful comment

The derivatives plugin has been implemented on master and will be available in Shrine 3.0.

https://github.com/shrinerb/shrine/blob/master/doc/plugins/derivatives.md#readme

>All comments

The derivatives plugin has been implemented on master and will be available in Shrine 3.0.

https://github.com/shrinerb/shrine/blob/master/doc/plugins/derivatives.md#readme

Was this page helpful?
0 / 5 - 0 ratings