shrine 2.14.0 → 2.15.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of shrine might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +384 -374
- data/README.md +132 -63
- data/doc/advantages.md +191 -109
- data/doc/attacher.md +1 -1
- data/doc/carrierwave.md +4 -4
- data/doc/creating_storages.md +2 -2
- data/doc/design.md +2 -2
- data/doc/direct_s3.md +3 -3
- data/doc/metadata.md +1 -1
- data/doc/multiple_files.md +2 -2
- data/doc/paperclip.md +3 -3
- data/doc/plugins/activerecord.md +92 -0
- data/doc/plugins/add_metadata.md +93 -0
- data/doc/plugins/backgrounding.md +148 -0
- data/doc/plugins/backup.md +29 -0
- data/doc/plugins/cached_attachment_data.md +23 -0
- data/doc/plugins/copy.md +22 -0
- data/doc/plugins/data_uri.md +92 -0
- data/doc/plugins/default_storage.md +16 -0
- data/doc/plugins/default_url.md +33 -0
- data/doc/plugins/default_url_options.md +22 -0
- data/doc/plugins/delete_promoted.md +10 -0
- data/doc/plugins/delete_raw.md +16 -0
- data/doc/plugins/derivation_endpoint.md +747 -0
- data/doc/plugins/determine_mime_type.md +64 -0
- data/doc/plugins/direct_upload.md +170 -0
- data/doc/plugins/download_endpoint.md +83 -0
- data/doc/plugins/dynamic_storage.md +20 -0
- data/doc/plugins/hooks.md +56 -0
- data/doc/plugins/included.md +15 -0
- data/doc/plugins/infer_extension.md +57 -0
- data/doc/plugins/keep_files.md +20 -0
- data/doc/plugins/logging.md +39 -0
- data/doc/plugins/metadata_attribues.md +43 -0
- data/doc/plugins/migration_helpers.md +58 -0
- data/doc/plugins/module_include.md +40 -0
- data/doc/plugins/moving.md +17 -0
- data/doc/plugins/multi_delete.md +18 -0
- data/doc/plugins/parallelize.md +14 -0
- data/doc/plugins/parsed_json.md +9 -0
- data/doc/plugins/presign_endpoint.md +133 -0
- data/doc/plugins/pretty_location.md +29 -0
- data/doc/plugins/processing.md +68 -0
- data/doc/plugins/rack_file.md +49 -0
- data/doc/plugins/rack_response.md +96 -0
- data/doc/plugins/recache.md +27 -0
- data/doc/plugins/refresh_metadata.md +31 -0
- data/doc/plugins/remote_url.md +104 -0
- data/doc/plugins/remove_attachment.md +16 -0
- data/doc/plugins/remove_invalid.md +9 -0
- data/doc/plugins/restore_cached_data.md +14 -0
- data/doc/plugins/sequel.md +64 -0
- data/doc/plugins/signature.md +49 -0
- data/doc/plugins/store_dimensions.md +68 -0
- data/doc/plugins/tempfile.md +40 -0
- data/doc/plugins/upload_endpoint.md +123 -0
- data/doc/plugins/upload_options.md +28 -0
- data/doc/plugins/validation_helpers.md +129 -0
- data/doc/plugins/versions.md +179 -0
- data/doc/processing.md +217 -247
- data/doc/refile.md +3 -3
- data/doc/release_notes/1.0.0.md +143 -0
- data/doc/release_notes/1.1.0.md +184 -0
- data/doc/release_notes/1.2.0.md +37 -0
- data/doc/release_notes/1.3.0.md +90 -0
- data/doc/release_notes/1.4.0.md +167 -0
- data/doc/release_notes/1.4.1.md +9 -0
- data/doc/release_notes/1.4.2.md +20 -0
- data/doc/release_notes/2.0.0.md +173 -0
- data/doc/release_notes/2.0.1.md +12 -0
- data/doc/release_notes/2.1.0.md +59 -0
- data/doc/release_notes/2.1.1.md +8 -0
- data/doc/release_notes/2.10.0.md +52 -0
- data/doc/release_notes/2.10.1.md +6 -0
- data/doc/release_notes/2.11.0.md +69 -0
- data/doc/release_notes/2.12.0.md +65 -0
- data/doc/release_notes/2.13.0.md +146 -0
- data/doc/release_notes/2.14.0.md +278 -0
- data/doc/release_notes/2.15.0.md +82 -0
- data/doc/release_notes/2.2.0.md +98 -0
- data/doc/release_notes/2.3.0.md +50 -0
- data/doc/release_notes/2.3.1.md +10 -0
- data/doc/release_notes/2.4.0.md +87 -0
- data/doc/release_notes/2.4.1.md +29 -0
- data/doc/release_notes/2.5.0.md +130 -0
- data/doc/release_notes/2.6.0.md +254 -0
- data/doc/release_notes/2.6.1.md +14 -0
- data/doc/release_notes/2.7.0.md +180 -0
- data/doc/release_notes/2.8.0.md +95 -0
- data/doc/release_notes/2.9.0.md +82 -0
- data/doc/retrieving_uploads.md +1 -1
- data/doc/storage/file_system.md +96 -0
- data/doc/storage/s3.md +293 -0
- data/doc/validation.md +1 -1
- data/lib/shrine/plugins/_urlsafe_serialization.rb +33 -125
- data/lib/shrine/plugins/activerecord.rb +0 -78
- data/lib/shrine/plugins/add_metadata.rb +0 -80
- data/lib/shrine/plugins/backgrounding.rb +0 -134
- data/lib/shrine/plugins/backup.rb +0 -22
- data/lib/shrine/plugins/cached_attachment_data.rb +0 -15
- data/lib/shrine/plugins/copy.rb +0 -14
- data/lib/shrine/plugins/data_uri.rb +0 -73
- data/lib/shrine/plugins/default_storage.rb +0 -11
- data/lib/shrine/plugins/default_url.rb +0 -25
- data/lib/shrine/plugins/default_url_options.rb +0 -16
- data/lib/shrine/plugins/delete_promoted.rb +0 -6
- data/lib/shrine/plugins/delete_raw.rb +0 -10
- data/lib/shrine/plugins/derivation_endpoint.rb +652 -0
- data/lib/shrine/plugins/determine_mime_type.rb +1 -85
- data/lib/shrine/plugins/direct_upload.rb +0 -155
- data/lib/shrine/plugins/download_endpoint.rb +11 -73
- data/lib/shrine/plugins/dynamic_storage.rb +0 -17
- data/lib/shrine/plugins/hooks.rb +0 -48
- data/lib/shrine/plugins/included.rb +0 -12
- data/lib/shrine/plugins/infer_extension.rb +0 -49
- data/lib/shrine/plugins/keep_files.rb +0 -19
- data/lib/shrine/plugins/logging.rb +0 -39
- data/lib/shrine/plugins/metadata_attributes.rb +0 -35
- data/lib/shrine/plugins/migration_helpers.rb +0 -50
- data/lib/shrine/plugins/module_include.rb +0 -32
- data/lib/shrine/plugins/moving.rb +0 -12
- data/lib/shrine/plugins/multi_delete.rb +0 -13
- data/lib/shrine/plugins/parallelize.rb +0 -8
- data/lib/shrine/plugins/parsed_json.rb +0 -5
- data/lib/shrine/plugins/presign_endpoint.rb +2 -117
- data/lib/shrine/plugins/pretty_location.rb +0 -22
- data/lib/shrine/plugins/processing.rb +0 -55
- data/lib/shrine/plugins/rack_file.rb +0 -39
- data/lib/shrine/plugins/rack_response.rb +0 -81
- data/lib/shrine/plugins/recache.rb +0 -21
- data/lib/shrine/plugins/refresh_metadata.rb +0 -24
- data/lib/shrine/plugins/remote_url.rb +0 -85
- data/lib/shrine/plugins/remove_attachment.rb +0 -10
- data/lib/shrine/plugins/remove_invalid.rb +0 -6
- data/lib/shrine/plugins/restore_cached_data.rb +0 -10
- data/lib/shrine/plugins/sequel.rb +0 -54
- data/lib/shrine/plugins/signature.rb +0 -37
- data/lib/shrine/plugins/store_dimensions.rb +0 -63
- data/lib/shrine/plugins/tempfile.rb +4 -35
- data/lib/shrine/plugins/upload_endpoint.rb +2 -109
- data/lib/shrine/plugins/upload_options.rb +0 -20
- data/lib/shrine/plugins/validation_helpers.rb +0 -36
- data/lib/shrine/plugins/versions.rb +0 -156
- data/lib/shrine/storage/file_system.rb +0 -77
- data/lib/shrine/storage/s3.rb +0 -249
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +2 -2
- metadata +86 -6
@@ -2,28 +2,6 @@
|
|
2
2
|
|
3
3
|
class Shrine
|
4
4
|
module Plugins
|
5
|
-
# The `backup` plugin allows you to automatically back up stored files to
|
6
|
-
# an additional storage.
|
7
|
-
#
|
8
|
-
# storages[:backup_store] = Shrine::Storage::S3.new(options)
|
9
|
-
# plugin :backup, storage: :backup_store
|
10
|
-
#
|
11
|
-
# After a file is stored, it will be reuploaded from store to the provided
|
12
|
-
# backup storage.
|
13
|
-
#
|
14
|
-
# user.update(avatar: file) # uploaded both to :store and :backup_store
|
15
|
-
#
|
16
|
-
# By default whenever stored files are deleted backed up files are deleted
|
17
|
-
# as well, but you can keep files on the "backup" storage by passing
|
18
|
-
# `delete: false`:
|
19
|
-
#
|
20
|
-
# plugin :backup, storage: :backup_store, delete: false
|
21
|
-
#
|
22
|
-
# Note that when adding this plugin with already existing stored files,
|
23
|
-
# Shrine won't know whether a stored file is backed up or not, so
|
24
|
-
# attempting to delete the backup could result in an error. To avoid that
|
25
|
-
# you can set `delete: false` until you manually back up the existing
|
26
|
-
# stored files.
|
27
5
|
module Backup
|
28
6
|
def self.configure(uploader, opts = {})
|
29
7
|
uploader.opts[:backup_storage] = opts.fetch(:storage, uploader.opts[:backup_storage])
|
@@ -2,21 +2,6 @@
|
|
2
2
|
|
3
3
|
class Shrine
|
4
4
|
module Plugins
|
5
|
-
# The `cached_attachment_data` plugin adds the ability to retain the cached
|
6
|
-
# file across form redisplays, which means the file doesn't have to be
|
7
|
-
# reuploaded in case of validation errors.
|
8
|
-
#
|
9
|
-
# plugin :cached_attachment_data
|
10
|
-
#
|
11
|
-
# The plugin adds `#cached_<attachment>_data` to the model, which returns
|
12
|
-
# the cached file as JSON, and should be used to set the value of the
|
13
|
-
# hidden form field.
|
14
|
-
#
|
15
|
-
# @user.cached_avatar_data #=> '{"id":"38k25.jpg","storage":"cache","metadata":{...}}'
|
16
|
-
#
|
17
|
-
# This method delegates to `Attacher#read_cached`:
|
18
|
-
#
|
19
|
-
# attacher.read_cached #=> '{"id":"38k25.jpg","storage":"cache","metadata":{...}}'
|
20
5
|
module CachedAttachmentData
|
21
6
|
module AttachmentMethods
|
22
7
|
def initialize(*)
|
data/lib/shrine/plugins/copy.rb
CHANGED
@@ -2,20 +2,6 @@
|
|
2
2
|
|
3
3
|
class Shrine
|
4
4
|
module Plugins
|
5
|
-
# The `copy` plugin allows copying attachment from one record to another.
|
6
|
-
#
|
7
|
-
# plugin :copy
|
8
|
-
#
|
9
|
-
# It adds a `Attacher#copy` method, which accepts another attacher, and
|
10
|
-
# copies the attachment from it:
|
11
|
-
#
|
12
|
-
# photo.image_attacher.copy(other_photo.image_attacher)
|
13
|
-
#
|
14
|
-
# This method will automatically be called when the record is duplicated:
|
15
|
-
#
|
16
|
-
# duplicated_photo = photo.dup
|
17
|
-
# duplicated_photo.image #=> #<Shrine::UploadedFile>
|
18
|
-
# duplicated_photo.image != photo.image
|
19
5
|
module Copy
|
20
6
|
module AttachmentMethods
|
21
7
|
def initialize(*)
|
@@ -8,79 +8,6 @@ require "forwardable"
|
|
8
8
|
|
9
9
|
class Shrine
|
10
10
|
module Plugins
|
11
|
-
# The `data_uri` plugin enables you to upload files as [data URIs].
|
12
|
-
# This plugin is useful for example when using [HTML5 Canvas].
|
13
|
-
#
|
14
|
-
# plugin :data_uri
|
15
|
-
#
|
16
|
-
# If your attachment is called "avatar", this plugin will add
|
17
|
-
# `#avatar_data_uri` and `#avatar_data_uri=` methods to your model.
|
18
|
-
#
|
19
|
-
# user.avatar #=> nil
|
20
|
-
# user.avatar_data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA"
|
21
|
-
# user.avatar #=> #<Shrine::UploadedFile>
|
22
|
-
#
|
23
|
-
# user.avatar.mime_type #=> "image/png"
|
24
|
-
# user.avatar.size #=> 43423
|
25
|
-
#
|
26
|
-
# You can also use `#data_uri=` and `#data_uri` methods directly on the
|
27
|
-
# `Shrine::Attacher` (which the model methods just delegate to):
|
28
|
-
#
|
29
|
-
# attacher.data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA"
|
30
|
-
#
|
31
|
-
# If the data URI wasn't correctly parsed, an error message will be added to
|
32
|
-
# the attachment column. You can change the default error message:
|
33
|
-
#
|
34
|
-
# plugin :data_uri, error_message: "data URI was invalid"
|
35
|
-
# plugin :data_uri, error_message: ->(uri) { I18n.t("errors.data_uri_invalid") }
|
36
|
-
#
|
37
|
-
# ## File extension
|
38
|
-
#
|
39
|
-
# A data URI doesn't convey any information about the file extension, so
|
40
|
-
# when attaching from a data URI, the uploaded file location will be
|
41
|
-
# missing an extension. If you want the upload location to always have an
|
42
|
-
# extension, you can load the `infer_extension` plugin to infer it from the
|
43
|
-
# MIME type.
|
44
|
-
#
|
45
|
-
# plugin :infer_extension
|
46
|
-
#
|
47
|
-
# ## `Shrine.data_uri`
|
48
|
-
#
|
49
|
-
# If you just want to parse the data URI and create an IO object from it,
|
50
|
-
# you can do that with `Shrine.data_uri`. If the data URI cannot be parsed,
|
51
|
-
# a `Shrine::Plugins::DataUri::ParseError` will be raised.
|
52
|
-
#
|
53
|
-
# # or YourUploader.data_uri("...")
|
54
|
-
# io = Shrine.data_uri("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA")
|
55
|
-
# io.content_type #=> "image/png"
|
56
|
-
# io.size #=> 21
|
57
|
-
# io.read # decoded content
|
58
|
-
#
|
59
|
-
# When the content type is ommited, `text/plain` is assumed. The parser
|
60
|
-
# also supports raw data URIs which aren't base64-encoded.
|
61
|
-
#
|
62
|
-
# # or YourUploader.data_uri("...")
|
63
|
-
# io = Shrine.data_uri("data:,raw%20content")
|
64
|
-
# io.content_type #=> "text/plain"
|
65
|
-
# io.size #=> 11
|
66
|
-
# io.read #=> "raw content"
|
67
|
-
#
|
68
|
-
# You can also assign a filename:
|
69
|
-
#
|
70
|
-
# io = Shrine.data_uri("data:,content", filename: "foo.txt")
|
71
|
-
# io.original_filename #=> "foo.txt"
|
72
|
-
#
|
73
|
-
# ## `UploadedFile#data_uri` and `UploadedFile#base64`
|
74
|
-
#
|
75
|
-
# This plugin also adds UploadedFile#data_uri method, which returns a
|
76
|
-
# base64-encoded data URI of the file content, and UploadedFile#base64,
|
77
|
-
# which simply returns the file content base64-encoded.
|
78
|
-
#
|
79
|
-
# uploaded_file.data_uri #=> "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA"
|
80
|
-
# uploaded_file.base64 #=> "iVBORw0KGgoAAAANSUhEUgAAAAUA"
|
81
|
-
#
|
82
|
-
# [data URIs]: https://tools.ietf.org/html/rfc2397
|
83
|
-
# [HTML5 Canvas]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
|
84
11
|
module DataUri
|
85
12
|
class ParseError < Error; end
|
86
13
|
|
@@ -2,17 +2,6 @@
|
|
2
2
|
|
3
3
|
class Shrine
|
4
4
|
module Plugins
|
5
|
-
# The `default_storage` plugin enables you to change which storages are going
|
6
|
-
# to be used for this uploader's attacher (the default is `:cache` and
|
7
|
-
# `:store`).
|
8
|
-
#
|
9
|
-
# plugin :default_storage, cache: :special_cache, store: :special_store
|
10
|
-
#
|
11
|
-
# You can also pass a block and choose the values depending on the record
|
12
|
-
# values and the name of the attachment. This is useful if you're using the
|
13
|
-
# `dynamic_storage` plugin. Example:
|
14
|
-
#
|
15
|
-
# plugin :default_storage, store: ->(record, name) { :"store_#{record.username}" }
|
16
5
|
module DefaultStorage
|
17
6
|
def self.configure(uploader, opts = {})
|
18
7
|
uploader.opts[:default_storage_cache] = opts.fetch(:cache, uploader.opts[:default_storage_cache])
|
@@ -2,31 +2,6 @@
|
|
2
2
|
|
3
3
|
class Shrine
|
4
4
|
module Plugins
|
5
|
-
# The `default_url` plugin allows setting the URL which will be returned when
|
6
|
-
# the attachment is missing.
|
7
|
-
#
|
8
|
-
# plugin :default_url
|
9
|
-
#
|
10
|
-
# Attacher.default_url do |options|
|
11
|
-
# "/#{name}/missing.jpg"
|
12
|
-
# end
|
13
|
-
#
|
14
|
-
# `Attacher#url` returns the default URL when attachment is missing. Any
|
15
|
-
# passed in URL options will be present in the `options` hash.
|
16
|
-
#
|
17
|
-
# attacher.url #=> "/avatar/missing.jpg"
|
18
|
-
# # or
|
19
|
-
# user.avatar_url #=> "/avatar/missing.jpg"
|
20
|
-
#
|
21
|
-
# The default URL block is evaluated in the context of an instance of
|
22
|
-
# `Shrine::Attacher`.
|
23
|
-
#
|
24
|
-
# Attacher.default_url do |options|
|
25
|
-
# self #=> #<Shrine::Attacher>
|
26
|
-
#
|
27
|
-
# name #=> :avatar
|
28
|
-
# record #=> #<User>
|
29
|
-
# end
|
30
5
|
module DefaultUrl
|
31
6
|
def self.configure(uploader, &block)
|
32
7
|
if block
|
@@ -2,22 +2,6 @@
|
|
2
2
|
|
3
3
|
class Shrine
|
4
4
|
module Plugins
|
5
|
-
# The `default_url_options` plugin allows you to specify URL options that
|
6
|
-
# will be applied by default for uploaded files of specified storages.
|
7
|
-
#
|
8
|
-
# plugin :default_url_options, store: { download: true }
|
9
|
-
#
|
10
|
-
# You can also generate the default URL options dynamically by using a
|
11
|
-
# block, which will receive the UploadedFile object along with any options
|
12
|
-
# that were passed to `UploadedFile#url`.
|
13
|
-
#
|
14
|
-
# plugin :default_url_options, store: -> (io, **options) do
|
15
|
-
# { response_content_disposition: ContentDisposition.attachment(io.original_filename) }
|
16
|
-
# end
|
17
|
-
#
|
18
|
-
# In both cases the default options are merged with options passed to
|
19
|
-
# `UploadedFile#url`, and the latter will always have precedence over
|
20
|
-
# default options.
|
21
5
|
module DefaultUrlOptions
|
22
6
|
def self.configure(uploader, options = {})
|
23
7
|
uploader.opts[:default_url_options] ||= {}
|
@@ -2,12 +2,6 @@
|
|
2
2
|
|
3
3
|
class Shrine
|
4
4
|
module Plugins
|
5
|
-
# The `delete_promoted` plugin deletes files that have been promoted, after
|
6
|
-
# the record is saved. This means that cached files handled by the attacher
|
7
|
-
# will automatically get deleted once they're uploaded to store. This also
|
8
|
-
# applies to any other uploaded file passed to `Attacher#promote`.
|
9
|
-
#
|
10
|
-
# plugin :delete_promoted
|
11
5
|
module DeletePromoted
|
12
6
|
module AttacherMethods
|
13
7
|
def promote(uploaded_file = get, **options)
|
@@ -2,16 +2,6 @@
|
|
2
2
|
|
3
3
|
class Shrine
|
4
4
|
module Plugins
|
5
|
-
# The `delete_raw` plugin will automatically delete raw files that have been
|
6
|
-
# uploaded. This is especially useful when doing processing, to ensure that
|
7
|
-
# temporary files have been deleted after upload.
|
8
|
-
#
|
9
|
-
# plugin :delete_raw
|
10
|
-
#
|
11
|
-
# By default any raw file that was uploaded will be deleted, but you can
|
12
|
-
# limit this only to files uploaded to certain storages:
|
13
|
-
#
|
14
|
-
# plugin :delete_raw, storages: [:store]
|
15
5
|
module DeleteRaw
|
16
6
|
def self.configure(uploader, opts = {})
|
17
7
|
uploader.opts[:delete_raw_storages] = opts.fetch(:storages, uploader.opts[:delete_raw_storages])
|
@@ -0,0 +1,652 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack"
|
4
|
+
require "content_disposition"
|
5
|
+
|
6
|
+
require "openssl"
|
7
|
+
require "tempfile"
|
8
|
+
|
9
|
+
class Shrine
|
10
|
+
module Plugins
|
11
|
+
module DerivationEndpoint
|
12
|
+
def self.load_dependencies(uploader, opts = {})
|
13
|
+
uploader.plugin :rack_response
|
14
|
+
uploader.plugin :_urlsafe_serialization
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.configure(uploader, opts = {})
|
18
|
+
uploader.opts[:derivation_endpoint_options] ||= {}
|
19
|
+
uploader.opts[:derivation_endpoint_options].merge!(opts)
|
20
|
+
|
21
|
+
uploader.opts[:derivation_endpoint_derivations] ||= {}
|
22
|
+
|
23
|
+
unless uploader.opts[:derivation_endpoint_options][:secret_key]
|
24
|
+
fail Error, "must provide :secret_key option to derivation_endpoint plugin"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module ClassMethods
|
29
|
+
def derivation_endpoint(**options)
|
30
|
+
Shrine::DerivationEndpoint.new(shrine_class: self, options: options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def derivation_response(env, **options)
|
34
|
+
script_name = env["SCRIPT_NAME"]
|
35
|
+
path_info = env["PATH_INFO"]
|
36
|
+
|
37
|
+
prefix = derivation_options[:prefix]
|
38
|
+
match = path_info.match(/^\/#{prefix}/)
|
39
|
+
|
40
|
+
fail Error, "request path must start with \"/#{prefix}\", but is \"#{path_info}\"" unless match
|
41
|
+
|
42
|
+
begin
|
43
|
+
env["SCRIPT_NAME"] += match.to_s
|
44
|
+
env["PATH_INFO"] = match.post_match
|
45
|
+
|
46
|
+
derivation_endpoint(**options).call(env)
|
47
|
+
ensure
|
48
|
+
env["SCRIPT_NAME"] = script_name
|
49
|
+
env["PATH_INFO"] = path_info
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def derivation(name, &block)
|
54
|
+
derivations[name] = block
|
55
|
+
end
|
56
|
+
|
57
|
+
def derivations
|
58
|
+
opts[:derivation_endpoint_derivations]
|
59
|
+
end
|
60
|
+
|
61
|
+
def derivation_options
|
62
|
+
opts[:derivation_endpoint_options]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
module FileMethods
|
67
|
+
def derivation_url(name, *args, **options)
|
68
|
+
derivation(name, *args).url(**options)
|
69
|
+
end
|
70
|
+
|
71
|
+
def derivation_response(name, *args, env:, **options)
|
72
|
+
derivation(name, *args, **options).response(env)
|
73
|
+
end
|
74
|
+
|
75
|
+
def derivation(name, *args, **options)
|
76
|
+
Shrine::Derivation.new(
|
77
|
+
name: name,
|
78
|
+
args: args,
|
79
|
+
source: self,
|
80
|
+
options: options,
|
81
|
+
)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
register_plugin(:derivation_endpoint, DerivationEndpoint)
|
87
|
+
end
|
88
|
+
|
89
|
+
class Derivation
|
90
|
+
class NotFound < Error; end
|
91
|
+
class SourceNotFound < Error; end
|
92
|
+
|
93
|
+
attr_reader :name, :args, :source, :options
|
94
|
+
|
95
|
+
def initialize(name:, args:, source:, options:)
|
96
|
+
@name = name
|
97
|
+
@args = args
|
98
|
+
@source = source
|
99
|
+
@options = options
|
100
|
+
end
|
101
|
+
|
102
|
+
def url(**options)
|
103
|
+
Derivation::Url.new(self).call(
|
104
|
+
host: option(:host),
|
105
|
+
prefix: option(:prefix),
|
106
|
+
expires_in: option(:expires_in),
|
107
|
+
version: option(:version),
|
108
|
+
metadata: option(:metadata),
|
109
|
+
**options,
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
def response(env)
|
114
|
+
Derivation::Response.new(self).call(env)
|
115
|
+
end
|
116
|
+
|
117
|
+
def processed
|
118
|
+
Derivation::Processed.new(self).call
|
119
|
+
end
|
120
|
+
|
121
|
+
def generate(file = nil)
|
122
|
+
Derivation::Generate.new(self).call(file)
|
123
|
+
end
|
124
|
+
|
125
|
+
def upload(file = nil)
|
126
|
+
Derivation::Upload.new(self).call(file)
|
127
|
+
end
|
128
|
+
|
129
|
+
def retrieve
|
130
|
+
Derivation::Retrieve.new(self).call
|
131
|
+
end
|
132
|
+
|
133
|
+
def delete
|
134
|
+
Derivation::Delete.new(self).call
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.options
|
138
|
+
@options ||= {}
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.option(name, default: nil, result: nil)
|
142
|
+
options[name] = { default: default, result: result }
|
143
|
+
end
|
144
|
+
|
145
|
+
option :cache_control, default: -> { default_cache_control }
|
146
|
+
option :disposition, default: -> { "inline" }
|
147
|
+
option :download, default: -> { true }
|
148
|
+
option :download_errors, default: -> { [] }
|
149
|
+
option :download_options, default: -> { {} }
|
150
|
+
option :expires_in
|
151
|
+
option :filename, default: -> { default_filename }
|
152
|
+
option :host
|
153
|
+
option :include_uploaded_file, default: -> { false }
|
154
|
+
option :metadata, default: -> { [] }
|
155
|
+
option :prefix
|
156
|
+
option :secret_key
|
157
|
+
option :type
|
158
|
+
option :upload, default: -> { false }
|
159
|
+
option :upload_location, default: -> { default_upload_location }, result: -> (o) { upload_location(o) }
|
160
|
+
option :upload_options, default: -> { {} }
|
161
|
+
option :upload_redirect, default: -> { false }
|
162
|
+
option :upload_redirect_url_options, default: -> { {} }
|
163
|
+
option :upload_storage, default: -> { source.storage_key.to_sym }
|
164
|
+
option :version
|
165
|
+
|
166
|
+
def option(name)
|
167
|
+
option_definition = self.class.options.fetch(name)
|
168
|
+
|
169
|
+
value = options.fetch(name) { shrine_class.derivation_options[name] }
|
170
|
+
value = instance_exec(&value) if value.is_a?(Proc)
|
171
|
+
|
172
|
+
if value.nil?
|
173
|
+
default = option_definition[:default]
|
174
|
+
value = instance_exec(&default) if default
|
175
|
+
end
|
176
|
+
|
177
|
+
result = option_definition[:result]
|
178
|
+
value = instance_exec(value, &result) if result
|
179
|
+
|
180
|
+
value
|
181
|
+
end
|
182
|
+
|
183
|
+
def shrine_class
|
184
|
+
source.shrine_class
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
# append version and extension to upload location if specified
|
190
|
+
def upload_location(location)
|
191
|
+
location = location.sub(/(?=(\.\w+)?$)/, "-#{option(:version)}") if option(:version)
|
192
|
+
location
|
193
|
+
end
|
194
|
+
|
195
|
+
def default_filename
|
196
|
+
[name, *args, File.basename(source.id, ".*")].join("-")
|
197
|
+
end
|
198
|
+
|
199
|
+
def default_upload_location
|
200
|
+
directory = source.id.sub(/\.[^\/]+/, "")
|
201
|
+
filename = [name, *args].join("-")
|
202
|
+
|
203
|
+
[directory, filename].join("/")
|
204
|
+
end
|
205
|
+
|
206
|
+
def default_cache_control
|
207
|
+
if option(:expires_in)
|
208
|
+
"public, max-age=#{option(:expires_in)}"
|
209
|
+
else
|
210
|
+
"public, max-age=#{365*24*60*60}"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
class Command
|
215
|
+
attr_reader :derivation
|
216
|
+
|
217
|
+
def initialize(derivation)
|
218
|
+
@derivation = derivation
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.delegate(*names)
|
222
|
+
names.each do |name|
|
223
|
+
protected define_method(name) {
|
224
|
+
if [:name, :args, :source].include?(name)
|
225
|
+
derivation.public_send(name)
|
226
|
+
else
|
227
|
+
derivation.option(name)
|
228
|
+
end
|
229
|
+
}
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
private
|
234
|
+
|
235
|
+
def shrine_class
|
236
|
+
derivation.shrine_class
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
class Derivation::Url < Derivation::Command
|
242
|
+
delegate :name, :args, :source, :secret_key
|
243
|
+
|
244
|
+
def call(host: nil, prefix: nil, **options)
|
245
|
+
[host, *prefix, identifier(**options)].join("/")
|
246
|
+
end
|
247
|
+
|
248
|
+
private
|
249
|
+
|
250
|
+
def identifier(expires_in: nil,
|
251
|
+
version: nil,
|
252
|
+
type: nil,
|
253
|
+
filename: nil,
|
254
|
+
disposition: nil,
|
255
|
+
metadata: [])
|
256
|
+
|
257
|
+
params = {}
|
258
|
+
params[:expires_at] = (Time.now.utc + expires_in).to_i if expires_in
|
259
|
+
params[:version] = version if version
|
260
|
+
params[:type] = type if type
|
261
|
+
params[:filename] = filename if filename
|
262
|
+
params[:disposition] = disposition if disposition
|
263
|
+
|
264
|
+
source_component = source.urlsafe_dump(metadata: metadata)
|
265
|
+
|
266
|
+
signed_url(name, *args, source_component, params)
|
267
|
+
end
|
268
|
+
|
269
|
+
def signed_url(*components)
|
270
|
+
signer = UrlSigner.new(secret_key)
|
271
|
+
signer.signed_url(*components)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
class DerivationEndpoint
|
276
|
+
attr_reader :shrine_class, :options
|
277
|
+
|
278
|
+
def initialize(shrine_class:, options: {})
|
279
|
+
@shrine_class = shrine_class
|
280
|
+
@options = options
|
281
|
+
end
|
282
|
+
|
283
|
+
def call(env)
|
284
|
+
request = Rack::Request.new(env)
|
285
|
+
|
286
|
+
status, headers, body = catch(:halt) do
|
287
|
+
error!(405, "Method not allowed") unless request.get? || request.head?
|
288
|
+
|
289
|
+
handle_request(request)
|
290
|
+
end
|
291
|
+
|
292
|
+
headers["Content-Length"] ||= body.map(&:bytesize).inject(0, :+).to_s
|
293
|
+
|
294
|
+
[status, headers, body]
|
295
|
+
end
|
296
|
+
|
297
|
+
def handle_request(request)
|
298
|
+
verify_signature!(request)
|
299
|
+
check_expiry!(request)
|
300
|
+
|
301
|
+
name, *args, serialized_file = request.path_info.split("/")[1..-1]
|
302
|
+
|
303
|
+
name = name.to_sym
|
304
|
+
uploaded_file = shrine_class::UploadedFile.urlsafe_load(serialized_file)
|
305
|
+
|
306
|
+
# request params override statically configured options
|
307
|
+
options = self.options.dup
|
308
|
+
|
309
|
+
options[:type] = request.params["type"] if request.params["type"]
|
310
|
+
options[:disposition] = request.params["disposition"] if request.params["disposition"]
|
311
|
+
options[:filename] = request.params["filename"] if request.params["filename"]
|
312
|
+
|
313
|
+
options[:expires_in] = expires_in(request) if request.params["expires_at"]
|
314
|
+
|
315
|
+
derivation = uploaded_file.derivation(name, *args, **options)
|
316
|
+
|
317
|
+
begin
|
318
|
+
status, headers, body = derivation.response(request.env)
|
319
|
+
rescue Derivation::NotFound
|
320
|
+
error!(404, "Unknown derivation \"#{name}\"")
|
321
|
+
rescue Derivation::SourceNotFound
|
322
|
+
error!(404, "Source file not found")
|
323
|
+
end
|
324
|
+
|
325
|
+
if status == 200 || status == 206
|
326
|
+
headers["Cache-Control"] = derivation.option(:cache_control)
|
327
|
+
end
|
328
|
+
|
329
|
+
[status, headers, body]
|
330
|
+
end
|
331
|
+
|
332
|
+
private
|
333
|
+
|
334
|
+
def verify_signature!(request)
|
335
|
+
signer = UrlSigner.new(secret_key)
|
336
|
+
signer.verify_url("#{request.path_info[1..-1]}?#{request.query_string}")
|
337
|
+
rescue UrlSigner::InvalidSignature => error
|
338
|
+
error!(403, error.message.capitalize)
|
339
|
+
end
|
340
|
+
|
341
|
+
def check_expiry!(request)
|
342
|
+
if request.params["expires_at"]
|
343
|
+
error!(403, "Request has expired") if expires_in(request) <= 0
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def expires_in(request)
|
348
|
+
expires_at = Integer(request.params["expires_at"])
|
349
|
+
|
350
|
+
(Time.at(expires_at) - Time.now).to_i
|
351
|
+
end
|
352
|
+
|
353
|
+
# Halts the request with the error message.
|
354
|
+
def error!(status, message)
|
355
|
+
throw :halt, [status, { "Content-Type" => "text/plain" }, [message]]
|
356
|
+
end
|
357
|
+
|
358
|
+
def secret_key
|
359
|
+
derivation_options[:secret_key]
|
360
|
+
end
|
361
|
+
|
362
|
+
def derivation_options
|
363
|
+
shrine_class.derivation_options.merge(self.options)
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
class Derivation::Response < Derivation::Command
|
368
|
+
delegate :type, :disposition, :filename,
|
369
|
+
:upload, :upload_redirect, :upload_redirect_url_options
|
370
|
+
|
371
|
+
def call(env)
|
372
|
+
if upload
|
373
|
+
upload_response(env)
|
374
|
+
else
|
375
|
+
local_response(env)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
private
|
380
|
+
|
381
|
+
def local_response(env)
|
382
|
+
derivative = derivation.generate
|
383
|
+
|
384
|
+
file_response(derivative, env)
|
385
|
+
end
|
386
|
+
|
387
|
+
def file_response(file, env)
|
388
|
+
file.close
|
389
|
+
response = rack_file_response(file.path, env)
|
390
|
+
|
391
|
+
status = response[0]
|
392
|
+
|
393
|
+
filename = self.filename
|
394
|
+
filename += File.extname(file.path) if File.extname(filename).empty?
|
395
|
+
|
396
|
+
headers = {}
|
397
|
+
headers["Content-Type"] = type || response[1]["Content-Type"]
|
398
|
+
headers["Content-Disposition"] = content_disposition(filename)
|
399
|
+
headers["Content-Length"] = response[1]["Content-Length"]
|
400
|
+
headers["Content-Range"] = response[1]["Content-Range"] if response[1]["Content-Range"]
|
401
|
+
headers["Accept-Ranges"] = "bytes"
|
402
|
+
|
403
|
+
body = Rack::BodyProxy.new(response[2]) { File.delete(file.path) }
|
404
|
+
|
405
|
+
[status, headers, body]
|
406
|
+
end
|
407
|
+
|
408
|
+
def upload_response(env)
|
409
|
+
uploaded_file = derivation.retrieve
|
410
|
+
|
411
|
+
unless uploaded_file
|
412
|
+
derivative = derivation.generate
|
413
|
+
uploaded_file = derivation.upload(derivative)
|
414
|
+
end
|
415
|
+
|
416
|
+
if upload_redirect
|
417
|
+
File.delete(derivative.path) if derivative
|
418
|
+
|
419
|
+
redirect_url = uploaded_file.url(upload_redirect_url_options)
|
420
|
+
|
421
|
+
[302, { "Location" => redirect_url }, []]
|
422
|
+
else
|
423
|
+
if derivative
|
424
|
+
file_response(derivative, env)
|
425
|
+
else
|
426
|
+
uploaded_file.to_rack_response(
|
427
|
+
type: type,
|
428
|
+
disposition: disposition,
|
429
|
+
filename: filename,
|
430
|
+
range: env["HTTP_RANGE"],
|
431
|
+
)
|
432
|
+
end
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
def rack_file_response(path, env)
|
437
|
+
server = Rack::File.new("", {}, "application/octet-stream")
|
438
|
+
|
439
|
+
if Rack.release > "2"
|
440
|
+
server.serving(Rack::Request.new(env), path)
|
441
|
+
else
|
442
|
+
server = server.dup
|
443
|
+
server.path = path
|
444
|
+
server.serving(env)
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
def content_disposition(filename)
|
449
|
+
ContentDisposition.format(disposition: disposition, filename: filename)
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
class Derivation::Processed < Derivation::Command
|
454
|
+
delegate :upload
|
455
|
+
|
456
|
+
def call
|
457
|
+
if upload
|
458
|
+
upload_result
|
459
|
+
else
|
460
|
+
local_result
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
private
|
465
|
+
|
466
|
+
def local_result
|
467
|
+
derivation.generate
|
468
|
+
end
|
469
|
+
|
470
|
+
def upload_result
|
471
|
+
uploaded_file = derivation.retrieve
|
472
|
+
|
473
|
+
unless uploaded_file
|
474
|
+
derivative = derivation.generate
|
475
|
+
uploaded_file = derivation.upload(derivative)
|
476
|
+
|
477
|
+
derivative.unlink
|
478
|
+
end
|
479
|
+
|
480
|
+
uploaded_file
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
class Derivation::Generate < Derivation::Command
|
485
|
+
delegate :name, :args, :source,
|
486
|
+
:download, :download_errors, :download_options,
|
487
|
+
:include_uploaded_file
|
488
|
+
|
489
|
+
def call(file = nil)
|
490
|
+
derivative = generate(file)
|
491
|
+
derivative = normalize(derivative)
|
492
|
+
derivative
|
493
|
+
end
|
494
|
+
|
495
|
+
private
|
496
|
+
|
497
|
+
def generate(file)
|
498
|
+
if download
|
499
|
+
with_downloaded(file) do |file|
|
500
|
+
if include_uploaded_file
|
501
|
+
uploader.instance_exec(file, source, *args, &derivation_block)
|
502
|
+
else
|
503
|
+
uploader.instance_exec(file, *args, &derivation_block)
|
504
|
+
end
|
505
|
+
end
|
506
|
+
else
|
507
|
+
uploader.instance_exec(source, *args, &derivation_block)
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
def normalize(derivative)
|
512
|
+
if derivative.is_a?(Tempfile)
|
513
|
+
derivative.open
|
514
|
+
elsif derivative.is_a?(File)
|
515
|
+
derivative.close
|
516
|
+
derivative = File.open(derivative.path)
|
517
|
+
elsif derivative.is_a?(String)
|
518
|
+
derivative = File.open(derivative)
|
519
|
+
elsif defined?(Pathname) && derivative.is_a?(Pathname)
|
520
|
+
derivative = derivative.open
|
521
|
+
else
|
522
|
+
fail Error, "unexpected derivation result: #{derivation.inspect} (expected File, Tempfile, String, or Pathname object)"
|
523
|
+
end
|
524
|
+
|
525
|
+
derivative.binmode
|
526
|
+
derivative
|
527
|
+
end
|
528
|
+
|
529
|
+
def with_downloaded(file, &block)
|
530
|
+
if file
|
531
|
+
yield file
|
532
|
+
else
|
533
|
+
download_source(&block)
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
def download_source
|
538
|
+
download_args = download_options.any? ? [download_options] : []
|
539
|
+
downloaded = false
|
540
|
+
|
541
|
+
source.download(*download_args) do |file|
|
542
|
+
downloaded = true
|
543
|
+
yield file
|
544
|
+
end
|
545
|
+
rescue *download_errors
|
546
|
+
raise if downloaded # re-raise if the error didn't happen on download
|
547
|
+
raise Derivation::SourceNotFound, "source file \"#{source.id}\" was not found on storage :#{source.storage_key}"
|
548
|
+
end
|
549
|
+
|
550
|
+
def derivation_block
|
551
|
+
shrine_class.derivations[name] or fail Derivation::NotFound, "derivation #{name.inspect} is not defined"
|
552
|
+
end
|
553
|
+
|
554
|
+
def uploader
|
555
|
+
source.uploader
|
556
|
+
end
|
557
|
+
end
|
558
|
+
|
559
|
+
class Derivation::Upload < Derivation::Command
|
560
|
+
delegate :upload_location, :upload_storage, :upload_options
|
561
|
+
|
562
|
+
def call(derivative = nil)
|
563
|
+
derivative ||= derivation.generate
|
564
|
+
|
565
|
+
uploader.upload derivative,
|
566
|
+
location: upload_location,
|
567
|
+
upload_options: upload_options
|
568
|
+
end
|
569
|
+
|
570
|
+
private
|
571
|
+
|
572
|
+
def uploader
|
573
|
+
shrine_class.new(upload_storage)
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
class Derivation::Retrieve < Derivation::Command
|
578
|
+
delegate :upload_location, :upload_storage
|
579
|
+
|
580
|
+
def call
|
581
|
+
if storage.exists?(upload_location)
|
582
|
+
shrine_class::UploadedFile.new(
|
583
|
+
"storage" => upload_storage.to_s,
|
584
|
+
"id" => upload_location,
|
585
|
+
)
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
private
|
590
|
+
|
591
|
+
def storage
|
592
|
+
shrine_class.find_storage(upload_storage)
|
593
|
+
end
|
594
|
+
end
|
595
|
+
|
596
|
+
class Derivation::Delete < Derivation::Command
|
597
|
+
delegate :upload_location, :upload_storage
|
598
|
+
|
599
|
+
def call
|
600
|
+
storage.delete(upload_location)
|
601
|
+
end
|
602
|
+
|
603
|
+
private
|
604
|
+
|
605
|
+
def storage
|
606
|
+
shrine_class.find_storage(upload_storage)
|
607
|
+
end
|
608
|
+
end
|
609
|
+
|
610
|
+
class UrlSigner
|
611
|
+
class InvalidSignature < Error; end
|
612
|
+
|
613
|
+
attr_reader :secret_key
|
614
|
+
|
615
|
+
def initialize(secret_key)
|
616
|
+
@secret_key = secret_key
|
617
|
+
end
|
618
|
+
|
619
|
+
def signed_url(*components, params)
|
620
|
+
path = Rack::Utils.escape_path(components.join("/"))
|
621
|
+
query = Rack::Utils.build_query(params)
|
622
|
+
|
623
|
+
signature = generate_signature("#{path}?#{query}")
|
624
|
+
|
625
|
+
query = Rack::Utils.build_query(params.merge(signature: signature))
|
626
|
+
|
627
|
+
"#{path}?#{query}"
|
628
|
+
end
|
629
|
+
|
630
|
+
def verify_url(path_with_query)
|
631
|
+
path, query = path_with_query.split("?")
|
632
|
+
|
633
|
+
params = Rack::Utils.parse_query(query.to_s)
|
634
|
+
signature = params.delete("signature")
|
635
|
+
query = Rack::Utils.build_query(params)
|
636
|
+
|
637
|
+
verify_signature("#{path}?#{query}", signature)
|
638
|
+
end
|
639
|
+
|
640
|
+
def verify_signature(string, signature)
|
641
|
+
if signature.nil?
|
642
|
+
fail InvalidSignature, "missing \"signature\" param"
|
643
|
+
elsif signature != generate_signature(string)
|
644
|
+
fail InvalidSignature, "provided signature does not match the calculated signature"
|
645
|
+
end
|
646
|
+
end
|
647
|
+
|
648
|
+
def generate_signature(string)
|
649
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret_key, string)
|
650
|
+
end
|
651
|
+
end
|
652
|
+
end
|