Last active
September 8, 2016 03:14
-
-
Save janko/c38a84af98008d226e5accb63bad506e to your computer and use it in GitHub Desktop.
Concatenation plugin for Shrine
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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