alchemy-dragonfly-s3 3.6.6 → 4.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 07e27d0972c3f7d205fea44ca18707a60674ba3a
4
- data.tar.gz: 4cbf280831ab503c0acac159cd26bd6f397e229d
2
+ SHA256:
3
+ metadata.gz: 331b499588960ebd9ead52ad551fba5e01677654abb26e069d0b368ad16817a1
4
+ data.tar.gz: 01ba7e860a18fd8e100a488734ead2fc0da2ae4448833a65a0214c19940e0aac
5
5
  SHA512:
6
- metadata.gz: d59f579103e7976a83f252389eb0a81ca82c4c0d852ecbc8f0d81f9c0e705b36decc74576eaa1157f25257e5c193a2dd3a10aa048b1174697eafd2adf485dcd2
7
- data.tar.gz: d386023a14465cf7d6f218f24fccdf3ff40165a6a2c031698ed9d9ab034c7b107a64d9944393192a59c048d6bbc09d2271f4d076cc186692a2d4aa4744bda22d
6
+ metadata.gz: 24ef9c786345029787101b8c305f6c23b195c1721ca58db62272cc60107c96c443e45e3f40aa9b32ce04417430fe9c4e3781ae7c9701f8a6f50d34357267cd90
7
+ data.tar.gz: c5d7f63a95a2fe8d9094da62c913c0887ca855f8b48f185b6221ff32f0dd0c5139b889d3155717027895b63f8c884b01ea42fa74c5332a601738202e65449f77
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Build Status](https://travis-ci.com/AlchemyCMS/alchemy-dragonfly-s3.svg?branch=alchemy-3)](https://travis-ci.com/AlchemyCMS/alchemy-dragonfly-s3)
1
+ [![Build Status](https://travis-ci.com/AlchemyCMS/alchemy-dragonfly-s3.svg?branch=alchemy-4)](https://travis-ci.com/AlchemyCMS/alchemy-dragonfly-s3)
2
2
 
3
3
  # AlchemyCMS AWS S3
4
4
 
@@ -6,16 +6,17 @@ Adds support for file attachments and rendered Alchemy thumbnails stored on Amaz
6
6
 
7
7
  ## Alchemy Version
8
8
 
9
- This branch works with Alchemy 3.6 only.
9
+ This branch works with Alchemy 4 only.
10
10
 
11
11
  - For a Alchemy 5 compatible version use the `master` branch.
12
+ - For a Alchemy 3.6 compatible version use the `alchemy-3` branch.
12
13
 
13
14
  ## Installation
14
15
 
15
16
  Add this line to your application's Gemfile:
16
17
 
17
18
  ```ruby
18
- gem 'alchemy-dragonfly-s3', github: 'AlchemyCMS/alchemy-dragonfly-s3', branch: 'alchemy-3'
19
+ gem 'alchemy-dragonfly-s3', github: 'AlchemyCMS/alchemy-dragonfly-s3', branch: 'alchemy-4'
19
20
  ```
20
21
 
21
22
  And then execute:
@@ -3,7 +3,7 @@
3
3
  module Alchemy
4
4
  module Dragonfly
5
5
  module S3
6
- VERSION = "3.6.6"
6
+ VERSION = "4.0.2"
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alchemy-dragonfly-s3
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.6.6
4
+ version: 4.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas von Deyen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-21 00:00:00.000000000 Z
11
+ date: 2020-07-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: alchemy_cms
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '3.6'
19
+ version: '4.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '3.6'
26
+ version: '4.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: dragonfly-s3_data_store
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -67,19 +67,19 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '4.0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: factory_girl_rails
70
+ name: factory_bot_rails
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '4.5'
75
+ version: '5'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '4.5'
82
+ version: '5'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: simplecov
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -117,21 +117,6 @@ extra_rdoc_files: []
117
117
  files:
118
118
  - MIT-LICENSE
119
119
  - README.md
120
- - app/assets/config/alchemy_dragonfly_s3_manifest.js
121
- - app/assets/images/alchemy/missing-image.svg
122
- - app/models/alchemy/attachment/s3_url.rb
123
- - app/models/alchemy/picture/s3_url.rb
124
- - app/models/alchemy/picture_thumb.rb
125
- - app/models/alchemy/picture_thumb/create.rb
126
- - app/models/alchemy/picture_thumb/signature.rb
127
- - app/models/alchemy/picture_thumb/uid.rb
128
- - app/models/alchemy/picture_variant.rb
129
- - app/views/alchemy/admin/attachments/show.html.erb
130
- - app/views/alchemy/admin/pictures/_picture.html.erb
131
- - app/views/alchemy/admin/pictures/_picture_to_assign.html.erb
132
- - app/views/alchemy/admin/pictures/show.html.erb
133
- - app/views/alchemy/essences/_essence_file_view.html.erb
134
- - db/migrate/1_create_alchemy_picture_thumbs.rb
135
120
  - lib/alchemy-dragonfly-s3.rb
136
121
  - lib/alchemy/attachment_monkey_patch.rb
137
122
  - lib/alchemy/dragonfly/s3/engine.rb
@@ -158,8 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
158
143
  - !ruby/object:Gem::Version
159
144
  version: '0'
160
145
  requirements: []
161
- rubyforge_project:
162
- rubygems_version: 2.6.14.4
146
+ rubygems_version: 3.0.3
163
147
  signing_key:
164
148
  specification_version: 4
165
149
  summary: AlchemyCMS Dragonfly S3.
@@ -1 +0,0 @@
1
- //= link alchemy/missing-image.svg
@@ -1 +0,0 @@
1
- <svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" width="16" height="16"><path fill="#f7f7f7" d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z" class=""></path></svg>
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Alchemy
4
- class Attachment < ActiveRecord::Base
5
- class S3Url
6
- def self.call(attachment)
7
- attachment.file.remote_url
8
- end
9
- end
10
- end
11
- end
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Alchemy
4
- class Picture < ActiveRecord::Base
5
- class S3Url
6
- attr_reader :variant
7
-
8
- # @param [Alchemy::PictureVariant]
9
- #
10
- def initialize(variant)
11
- raise ArgumentError, "Variant missing!" if variant.nil?
12
-
13
- @variant = variant
14
- end
15
-
16
- def call(*)
17
- return variant.image.remote_url unless processible_image?
18
-
19
- ::Dragonfly.app(:alchemy_pictures).remote_url_for(uid)
20
- end
21
-
22
- private
23
-
24
- def processible_image?
25
- variant.image.is_a?(::Dragonfly::Job)
26
- end
27
-
28
- def uid
29
- signature = PictureThumb::Signature.call(variant)
30
- thumb = variant.picture.thumbs.detect { |t| t.signature == signature }
31
- if thumb
32
- uid = thumb.uid
33
- else
34
- uid = PictureThumb::Uid.call(signature, variant)
35
- PictureThumb::Create.call(variant, signature, uid)
36
- end
37
- uid
38
- end
39
- end
40
- end
41
- end
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Alchemy
4
- # The persisted version of a rendered picture variant
5
- #
6
- class PictureThumb < ActiveRecord::Base
7
- belongs_to :picture, class_name: "Alchemy::Picture"
8
-
9
- validates :signature, presence: true
10
- validates :uid, presence: true
11
-
12
- class << self
13
- # Upfront generation of picture thumbnails
14
- #
15
- # Called after a Alchemy::Picture has been created (after an image has been uploaded)
16
- #
17
- # Generates three types of thumbnails that are used by Alchemys picture archive and
18
- # persists them in the configures file store (Default Dragonfly::FileDataStore).
19
- #
20
- # @see Picture::THUMBNAIL_SIZES
21
- def generate_thumbs!(picture)
22
- Alchemy::Picture::THUMBNAIL_SIZES.values.map do |size|
23
- variant = Alchemy::PictureVariant.new(picture, {
24
- size: size,
25
- flatten: true,
26
- })
27
- signature = Alchemy::PictureThumb::Signature.call(variant)
28
- thumb = find_by(signature: signature)
29
- next if thumb
30
-
31
- uid = Alchemy::PictureThumb::Uid.call(signature, variant)
32
- Alchemy::PictureThumb::Create.call(variant, signature, uid)
33
- uid
34
- end
35
- end
36
- end
37
- end
38
- end
@@ -1,28 +0,0 @@
1
- module Alchemy
2
- class PictureThumb < ActiveRecord::Base
3
- # Stores the render result of a Alchemy::PictureVariant
4
- # in the Dragonfly S3 datastore
5
- #
6
- class Create
7
- def self.call(variant, signature, uid)
8
- # create the thumb before uploading
9
- # to prevent db race conditions
10
- thumb = variant.picture.thumbs.create!(
11
- picture: variant.picture,
12
- signature: signature,
13
- uid: uid,
14
- )
15
- begin
16
- # fetch and process the image
17
- image = variant.image
18
- # upload the processed image
19
- image.store(path: uid)
20
- rescue RuntimeError, Excon::Error => e
21
- Rails.logger.warn(e)
22
- # destroy the thumb if processing or upload fails
23
- thumb.destroy
24
- end
25
- end
26
- end
27
- end
28
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Alchemy
4
- class PictureThumb < ActiveRecord::Base
5
- class Signature
6
- # Returns a unique image process signature
7
- #
8
- # @param [Alchemy::PictureVariant]
9
- #
10
- # @return [String]
11
- def self.call(variant)
12
- steps_without_fetch = variant.image.steps.reject do |step|
13
- step.is_a?(::Dragonfly::Job::Fetch)
14
- end
15
-
16
- steps_with_id = [[variant.picture.id]] + steps_without_fetch
17
- job_string = steps_with_id.map(&:to_a).to_dragonfly_unique_s
18
-
19
- Digest::SHA1.hexdigest(job_string)
20
- end
21
- end
22
- end
23
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Alchemy
4
- class PictureThumb < ActiveRecord::Base
5
- class Uid
6
- # Returns a image variant uid for storage
7
- #
8
- # @param [String]
9
- # @param [Alchemy::PictureVariant]
10
- #
11
- # @return [String]
12
- def self.call(signature, variant)
13
- picture = variant.picture
14
- filename = variant.image_file_name || "image"
15
- name = File.basename(filename, ".*").gsub(/[^\w.]+/, "_")
16
- ext = variant.render_format
17
-
18
- "pictures/#{picture.id}/#{signature}/#{name}.#{ext}"
19
- end
20
- end
21
- end
22
- end
@@ -1,114 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "forwardable"
4
-
5
- module Alchemy
6
- # Represents a rendered picture
7
- #
8
- # Resizes, crops and encodes the image with imagemagick
9
- #
10
- class PictureVariant
11
- extend Forwardable
12
-
13
- include Alchemy::Logger
14
- include Alchemy::Picture::Transformations
15
-
16
- attr_reader :picture, :render_format
17
-
18
- def_delegators :@picture,
19
- :image_file,
20
- :image_file_width,
21
- :image_file_height,
22
- :image_file_name,
23
- :image_file_size
24
-
25
- # @param [Alchemy::Picture]
26
- #
27
- # @param [Hash] options passed to the image processor
28
- # @option options [Boolean] :crop Pass true to enable cropping
29
- # @option options [String] :crop_from Coordinates to start cropping from
30
- # @option options [String] :crop_size Size of the cropping area
31
- # @option options [Boolean] :flatten Pass true to flatten GIFs
32
- # @option options [String|Symbol] :format Image format to encode the image in
33
- # @option options [Integer] :quality JPEG compress quality
34
- # @option options [String] :size Size of resulting image in WxH
35
- # @option options [Boolean] :upsample Pass true to upsample (grow) an image if the original size is lower than the resulting size
36
- #
37
- def initialize(picture, options = {})
38
- raise ArgumentError, "Picture missing!" if picture.nil?
39
-
40
- @picture = picture
41
- @options = options
42
- @render_format = options[:format] || picture.default_render_format
43
- end
44
-
45
- # Process a variant of picture
46
- #
47
- # @return [Dragonfly::Attachment|Dragonfly::Job] The processed image variant
48
- #
49
- def image
50
- image = image_file
51
-
52
- raise MissingImageFileError, "Missing image file for #{picture.inspect}" if image.nil?
53
-
54
- image = processed_image(image, @options)
55
- image = encoded_image(image, @options)
56
- image
57
- rescue MissingImageFileError, WrongImageFormatError => e
58
- log_warning(e.message)
59
- nil
60
- end
61
-
62
- private
63
-
64
- # Returns the processed image dependent of size and cropping parameters
65
- def processed_image(image, options = {})
66
- size = options[:size]
67
- upsample = !!options[:upsample]
68
-
69
- return image unless size.present? && picture.has_convertible_format?
70
-
71
- if options[:crop]
72
- crop(size, options[:crop_from], options[:crop_size], upsample)
73
- else
74
- resize(size, upsample)
75
- end
76
- end
77
-
78
- # Returns the encoded image
79
- #
80
- # Flatten animated gifs, only if converting to a different format.
81
- # Can be overwritten via +options[:flatten]+.
82
- #
83
- def encoded_image(image, options = {})
84
- unless render_format.in?(Alchemy::Picture.allowed_filetypes)
85
- raise WrongImageFormatError.new(picture, @render_format)
86
- end
87
-
88
- options = {
89
- flatten: render_format != "gif" && picture.image_file_format == "gif",
90
- }.with_indifferent_access.merge(options)
91
-
92
- encoding_options = []
93
-
94
- convert_format = render_format.sub("jpeg", "jpg") != picture.image_file_format.sub("jpeg", "jpg")
95
-
96
- if render_format =~ /jpe?g/ && convert_format
97
- quality = options[:quality] || Config.get(:output_image_jpg_quality)
98
- encoding_options << "-quality #{quality}"
99
- end
100
-
101
- if options[:flatten]
102
- encoding_options << "-flatten"
103
- end
104
-
105
- convertion_needed = convert_format || encoding_options.present?
106
-
107
- if picture.has_convertible_format? && convertion_needed
108
- image = image.encode(render_format, encoding_options.join(" "))
109
- end
110
-
111
- image
112
- end
113
- end
114
- end
@@ -1,55 +0,0 @@
1
- <div class="resource_info">
2
- <div class="value">
3
- <label>
4
- <%= render_icon @attachment.icon_css_class %>
5
- </label>
6
- <p><%= @attachment.file_name %></p>
7
- </div>
8
- <div class="value with-icon">
9
- <label><%= Alchemy::Attachment.human_attribute_name(:url) %></label>
10
- <p><%= @attachment.url %></p>
11
- <a data-clipboard-text="<%= @attachment.url %>" class="icon_button--right">
12
- <%= render_icon('clipboard full') %>
13
- </a>
14
- </div>
15
- <div class="value with-icon">
16
- <label><%= Alchemy::Attachment.human_attribute_name(:download_url) %></label>
17
- <p><%= @attachment.url %></p>
18
- <a data-clipboard-text="<%= @attachment.url %>" class="icon_button--right">
19
- <%= render_icon('clipboard full') %>
20
- </a>
21
- </div>
22
- </div>
23
-
24
- <% case @attachment.icon_css_class %>
25
- <% when "image" %>
26
- <div class="attachment_preview_container image-preview">
27
- <%= image_tag(@attachment.url, class: "full_width") %>
28
- </div>
29
- <% when "audio" %>
30
- <div class="attachment_preview_container player-preview">
31
- <%= audio_tag(@attachment.url, preload: "none", controls: true, class: "full_width") %>
32
- </div>
33
- <% when "video" %>
34
- <div class="attachment_preview_container player-preview">
35
- <%= video_tag(@attachment.url, preload: "metadata", controls: true, class: "full_width") %>
36
- </div>
37
- <% when "archive", "file" %>
38
- <% else %>
39
- <iframe src="<%= @attachment.url %>" frameborder=0 class="full-iframe">
40
- Your browser does not support frames.
41
- </iframe>
42
- <% end %>
43
-
44
- <script type="text/javascript">
45
- $(function() {
46
- var clipboard = new Clipboard('.icon_button--right');
47
- clipboard.on('success', function(e) {
48
- Alchemy.growl('<%= Alchemy.t("Copied to clipboard") %>');
49
- e.clearSelection();
50
- });
51
- Alchemy.currentDialog().dialog.on('DialogClose.Alchemy', function() {
52
- clipboard.destroy();
53
- });
54
- });
55
- </script>
@@ -1,53 +0,0 @@
1
- <div class="picture_thumbnail <%= @size %>" id="picture_<%= picture.id %>" name="<%= picture.name %>">
2
- <span class="picture_tool select">
3
- <%= check_box_tag "picture_ids[]", picture.id %>
4
- </span>
5
- <% if picture.deletable? && can?(:destroy, picture) %>
6
- <span class="picture_tool delete">
7
- <%= link_to_confirm_dialog(
8
- "",
9
- Alchemy.t(:confirm_to_delete_image_from_server),
10
- alchemy.admin_picture_path(
11
- id: picture,
12
- q: params[:q],
13
- page: params[:page],
14
- tagged_with: params[:tagged_with],
15
- size: params[:size],
16
- filter: params[:filter]
17
- ),
18
- {
19
- title: Alchemy.t('Delete image')
20
- }
21
- ) -%>
22
- </span>
23
- <% end %>
24
- <% image = image_tag(
25
- picture.url(size: preview_size(@size), flatten: true) || "alchemy/missing-image.svg",
26
- alt: picture.name,
27
- title: Alchemy.t(:zoom_image)
28
- ) %>
29
- <% if can?(:edit, picture) %>
30
- <%= link_to(
31
- image,
32
- alchemy.admin_picture_path(
33
- id: picture,
34
- q: params[:q],
35
- page: params[:page],
36
- tagged_with: params[:tagged_with],
37
- size: params[:size],
38
- filter: params[:filter]
39
- ),
40
- class: 'thumbnail_background'
41
- ) %>
42
- <% else %>
43
- <%= image %>
44
- <% end %>
45
- <span class="picture_name" title="<%= picture.name %>">
46
- <%= picture.name %>
47
- </span>
48
- <div class="picture_tags">
49
- <% picture.tag_list.each do |tag| %>
50
- <span class="tag"><%= tag %></span>
51
- <% end %>
52
- </div>
53
- </div>
@@ -1,18 +0,0 @@
1
- <div class="picture_thumbnail assign_image_list_detail <%= size.blank? ? 'medium' : size %>" name="<%= picture_to_assign.name %>" id="picture_to_assign_<%= picture_to_assign.id %>">
2
- <% action_url = create_or_assign_url(picture_to_assign, @options.to_json) %>
3
- <%= link_to(
4
- image_tag(
5
- picture_to_assign.url(size: preview_size(size), flatten: true) || "alchemy/missing-image.svg",
6
- alt: picture_to_assign.name
7
- ),
8
- action_url,
9
- remote: true,
10
- onclick: '$(self).attr("href", "#").off("click"); return false',
11
- method: @content.blank? ? 'post' : 'put',
12
- title: Alchemy.t(:assign_image),
13
- class: 'thumbnail_background'
14
- ) %>
15
- <div class="picture_name" title="<%= picture_to_assign.name %>">
16
- <%= picture_to_assign.name %>
17
- </div>
18
- </div>
@@ -1,43 +0,0 @@
1
- <div class="zoomed-picture-background">
2
- <%= image_tag @picture.url || "alchemy/missing-image.svg" %>
3
- </div>
4
-
5
- <div class="picture-overlay-navigation">
6
- <% if @previous %>
7
- <%= link_to alchemy.admin_picture_path(
8
- id: @previous,
9
- q: params[:q],
10
- page: params[:page],
11
- tagged_with: params[:tagged_with],
12
- size: params[:size],
13
- filter: params[:filter]
14
- ),
15
- class: "previous-picture",
16
- remote: true do %>
17
- <span class="icon-angle-left"></span>
18
- <% end %>
19
- <% end %>
20
- <% if @next %>
21
- <%= link_to alchemy.admin_picture_path(
22
- id: @next,
23
- q: params[:q],
24
- page: params[:page],
25
- tagged_with: params[:tagged_with],
26
- size: params[:size],
27
- filter: params[:filter]
28
- ),
29
- class: "next-picture",
30
- remote: true do %>
31
- <span class="icon-angle-right"></span>
32
- <% end %>
33
- <% end %>
34
- </div>
35
-
36
- <div class="picture-details-overlay">
37
- <%= render 'form' %>
38
- <%= render 'infos' %>
39
- </div>
40
-
41
- <div class="picture-overlay-handle">
42
- <span class="icon-angle-double-right"></span>
43
- </div>
@@ -1,13 +0,0 @@
1
- <%- if attachment = content.ingredient -%>
2
- <%- html_options = local_assigns.fetch(:html_options, {}) -%>
3
- <%= link_to(
4
- content.essence.link_text.presence ||
5
- content.settings_value(:link_text, local_assigns.fetch(:options, {})) ||
6
- attachment.name,
7
- attachment.url,
8
- {
9
- class: content.essence.css_class.presence,
10
- title: content.essence.title.presence
11
- }.merge(html_options)
12
- ) -%>
13
- <%- end -%>
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class CreateAlchemyPictureThumbs < ActiveRecord::Migration
4
- def change
5
- create_table :alchemy_picture_thumbs do |t|
6
- t.integer :picture_id, null: false, index: true
7
- t.string :signature, null: false
8
- t.text :uid, null: false
9
- end
10
- add_foreign_key :alchemy_picture_thumbs, :alchemy_pictures, column: :picture_id
11
- add_index :alchemy_picture_thumbs, :signature, unique: true
12
- end
13
- end