Skip to content

Instantly share code, notes, and snippets.

@janko
Last active September 8, 2016 03:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save janko/c38a84af98008d226e5accb63bad506e to your computer and use it in GitHub Desktop.
Save janko/c38a84af98008d226e5accb63bad506e to your computer and use it in GitHub Desktop.
Concatenation plugin for Shrine
class Shrine
module Plugins
# The `concatenation` plugin allows you to assign to the attacher a
# cached file which is composed of multiple uploaded parts. The plugin
# will then call `#concat` on the storage, which is expected to
# concatenate the given parts into a single file. The assigned
# attachment will then be a complete cached file.
#
# plugin :concatenation
#
# This plugin extends `Attacher#assign` so that it accepts cached files in
# JSON format with an additional `"parts"` field, which contains an array
# of uploaded parts:
#
# {
# "parts": [
# {"id": "aaa.jpg", "storage": "cache", "metadata": {}},
# {"id": "bbb.jpg", "storage": "cache", "metadata": {}},
# # ...
# ],
# "id": "lsdg94l31.jpg", # optional
# "metadata": { } # optional
# }
#
# You can also set additional concatenation options that will be passed
# directly to the storage:
#
# plugin :concatenation, options: {use_accelerate_endpoint: true}
#
# plugin :concatenation, options: ->(io, context) do
# {use_accelerate_endpoint: true} unless context[:record].guest?
# end
#
# Alternatively, you can call `Shrine#concat` directly, with an array of
# `Shrine::UploadedFile` objects
#
# parts #=> array of Shrine::UploadedFile objects
# uploader = ImageUploader.new(:store)
#
# uploader.concat(parts)
# # explicitly set the destination location
# uploader.concat(parts, "result.jpg")
# # set/override metadata for the concatenated file
# uploader.concat(parts, shrine_metadata: {"filename" => "result.jpg"})
# # send additional options to the concatenation request
# uploader.concat(parts, use_accelerate_endpoint: true)
#
# The `"metadata"` field of individual parts should contain information
# that your storage needs to perform concatenation, refer to the
# documentation of your storage.
module Concatenation
def self.configure(uploader, opts = {})
uploader.opts[:concatenation_options] = opts.fetch(:options, uploader.opts.fetch(:options, {}))
end
module AttacherMethods
def assign(value)
if value.is_a?(String) && !value.empty?
data = JSON.parse(value)
if data.key?("parts")
parts = data["parts"].map { |part_data| uploaded_file(part_data) }
metadata = data["metadata"] || {}
options = shrine_class.opts[:concatenation_options]
options = options.call(context) if options.respond_to?(:call)
options ||= {}
cached_file = cache.concat(parts, location, shrine_metadata: metadata, **options)
assign_cached(cached_file)
else
super
end
end
end
end
module InstanceMethods
def concat(parts, location = nil, shrine_metadata: {}, **options)
if parts.any?
location ||= generate_location(parts.first, {})
shrine_metadata["size"] ||= parts.map(&:size).inject(:+)
shrine_metadata["mime_type"] ||= parts.first.mime_type
shrine_metadata["filename"] ||= parts.first.original_filename
else
location ||= generate_uid(nil)
shrine_metadata["size"] ||= 0
end
storage.concat(parts, location, shrine_metadata: shrine_metadata, **options)
self.class.uploaded_file(
"id" => location,
"storage" => storage_key.to_s,
"metadata" => shrine_metadata,
)
end
end
end
register_plugin(:concatenation, Concatenation)
end
end
require "test_helper"
require "shrine/plugins/concatenation"
describe Shrine::Plugins::Concatenation do
before do
@attacher = attacher { plugin :concatenation }
@attacher.cache.storage.instance_eval do
def concat(parts, id, shrine_metadata: {}, **options)
content = parts.inject("") { |string, part| string << read(part.id) }
upload(StringIO.new(content), id)
end
end
options = {filename: "hello.txt", content_type: "text/plain"}
@parts = [
@attacher.cache!(fakeio("Hello", options)),
@attacher.cache!(fakeio(" World", options)),
@attacher.cache!(fakeio("!", options)),
]
end
describe "Attacher#assign" do
it "accepts parts" do
data = {
"parts" => @parts.map(&:data),
}
@attacher.assign(data.to_json)
attachment = @attacher.get
assert_equal "Hello World!", attachment.read
assert_match /\.txt$/, attachment.id
assert_equal "cache", attachment.storage_key
assert_equal 12, attachment.metadata["size"]
assert_equal "hello.txt", attachment.metadata["filename"]
assert_equal "text/plain", attachment.metadata["mime_type"]
end
it "accepts additional id" do
data = {
"parts" => @parts.map(&:data),
"id" => "foo.txt",
}
@attacher.assign(data.to_json)
attachment = @attacher.get
assert_equal "foo.txt", attachment.id
assert_equal "Hello World!", attachment.read
end
it "accepts additional metadata" do
data = {
"parts" => @parts.map(&:data),
"metadata" => {"mime_type" => "foo/bar", "foo" => "bar"},
}
@attacher.assign(data.to_json)
attachment = @attacher.get
assert_equal "foo/bar", attachment.metadata["mime_type"]
assert_equal "bar", attachment.metadata["foo"]
assert_equal 12, attachment.metadata["size"]
assert_equal "hello.txt", attachment.metadata["filename"]
end
it "accepts concatenation options as a hash" do
@attacher.shrine_class.plugin :concatenation, options: {foo: "bar"}
data = {"parts" => []}
@attacher.cache.storage.expects(:concat).with { |*args| args.last[:foo] == "bar" }
@attacher.assign(data.to_json)
end
it "accepts concatenation options as a block" do
@attacher.shrine_class.plugin :concatenation, options: ->(context) do
raise unless context.is_a?(Hash) && context.any?
{foo: "bar"}
end
data = {"parts" => []}
@attacher.cache.storage.expects(:concat).with { |*args| args.last[:foo] == "bar" }
@attacher.assign(data.to_json)
end
end
describe "Attacher#concat" do
it "assigns the file to the" do
# assertions
end
end
it "adds concat to Attacher" do
@attacher.concat(@parts, "foo", shrine_metadata: {"filename" => "bar.png"})
attachment = @attacher.get
assert_equal "Hello World!", attachment.read
assert_equal "foo", attachment.id
assert_equal "cache", attachment.storage_key
assert_equal 12, attachment.metadata["size"]
assert_equal "bar.png", attachment.metadata["filename"]
end
it "adds concat to Shrine" do
cached_file = @attacher.cache.concat(@parts, "foo", shrine_metadata: {"filename" => "bar.png"})
assert_equal "Hello World!", cached_file.read
assert_equal "foo", cached_file.id
assert_equal "cache", cached_file.storage_key
assert_equal 12, cached_file.metadata["size"]
assert_equal "bar.png", cached_file.metadata["filename"]
end
it "doesn't require location to be passed in" do
# assertions
end
it "doesn't set size if it was set by the storage" do
@attacher.cache.storage.instance_eval do
def concat(parts, id, shrine_metadata: {}, **options)
shrine_metadata["size"] = 3
end
end
cached_file = @attacher.cache.concat(@parts, "foo")
assert_equal 3, cached_file.size
end
it "accepts 0 parts" do
cached_file = @attacher.cache.concat([], "foo")
assert_equal "", cached_file.read
assert_equal 0, cached_file.metadata["size"]
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment