alchemy-dragonfly-s3 4.0.5 → 5.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6256465573974ec82c6ceee7c1b72484bce11c7194151f571718fd48b763f482
4
- data.tar.gz: 0b2b2e665659e02c81754287efb25e4025a678efceba5ae28462bac22faeb43a
3
+ metadata.gz: 07b85abda41c799474122920cea92e2fddfa003314bca025bf29ab055b62061c
4
+ data.tar.gz: 182e4d22390fee03f7c652fc6fe6ad925a811e1768ce09e2028315830cd72d00
5
5
  SHA512:
6
- metadata.gz: 3c3b5d672dde007412c96d300b7c533d13897f805877d42e1242c33e72e65d1e319214cebab1a86f806f9007fb20c2f24aee05b7fb2120ae3577a14663c5784a
7
- data.tar.gz: 87bac607d1459905241d76e77167c668c1c1a7a5cc10ffd85754f41a1ead60df500211d784c76d5eacac9455f8e39ddf06bc18e4df6fe514880aa04c56e01f30
6
+ metadata.gz: 82976ed6c263722910b05bbdd3cb0dd9a1434d1f767967c7ae49e9fa2eda75bff7fe9d9795f8af45d2718ce9afb0a00b4674c743f144de0db938992a630aebb4
7
+ data.tar.gz: 2c0fdccf15ac1fd8870e47255e51b90738bacb43828bef8edb19d9b46186fb10dfa1b5b6beb6ef08244b5d9e0db62210c95d897b5bca226147c256f9095b195b
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Build Status](https://travis-ci.com/AlchemyCMS/alchemy-dragonfly-s3.svg?branch=alchemy-4)](https://travis-ci.com/AlchemyCMS/alchemy-dragonfly-s3)
1
+ [![Build Status](https://travis-ci.com/AlchemyCMS/alchemy-dragonfly-s3.svg?branch=alchemy-5)](https://travis-ci.com/AlchemyCMS/alchemy-dragonfly-s3)
2
2
 
3
3
  # AlchemyCMS AWS S3
4
4
 
@@ -6,9 +6,10 @@ 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 4 only.
9
+ This branch works with Alchemy 5.0 only.
10
10
 
11
- - For a Alchemy 5 compatible version use the `master` branch.
11
+ - For a Alchemy 5.1 compatible version use the `master` branch.
12
+ - For a Alchemy 4 compatible version use the `alchemy-4` branch.
12
13
  - For a Alchemy 3.6 compatible version use the `alchemy-3` branch.
13
14
 
14
15
  ## Installation
@@ -16,7 +17,7 @@ This branch works with Alchemy 4 only.
16
17
  Add this line to your application's Gemfile:
17
18
 
18
19
  ```ruby
19
- gem 'alchemy-dragonfly-s3', github: 'AlchemyCMS/alchemy-dragonfly-s3', branch: 'alchemy-4'
20
+ gem 'alchemy-dragonfly-s3', github: 'AlchemyCMS/alchemy-dragonfly-s3', branch: 'alchemy-5'
20
21
  ```
21
22
 
22
23
  And then execute:
@@ -3,7 +3,7 @@
3
3
  module Alchemy
4
4
  module Dragonfly
5
5
  module S3
6
- VERSION = "4.0.5"
6
+ VERSION = "5.0.4"
7
7
  end
8
8
  end
9
9
  end
@@ -6,11 +6,11 @@ module Alchemy
6
6
  super || "missing-image.png"
7
7
  end
8
8
 
9
- def thumbnail_url(options = {})
9
+ def thumbnail_url
10
10
  super || "alchemy/missing-image.svg"
11
11
  end
12
12
 
13
- def allow_image_cropping?(options = {})
13
+ def allow_image_cropping?
14
14
  super && !!picture.image_file
15
15
  end
16
16
 
metadata CHANGED
@@ -1,29 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alchemy-dragonfly-s3
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.5
4
+ version: 5.0.4
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-20 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
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '4.0'
19
+ version: 5.0.0.beta1
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '5.1'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - "~>"
27
+ - - ">="
25
28
  - !ruby/object:Gem::Version
26
- version: '4.0'
29
+ version: 5.0.0.beta1
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.1'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: dragonfly-s3_data_store
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -117,21 +123,6 @@ extra_rdoc_files: []
117
123
  files:
118
124
  - MIT-LICENSE
119
125
  - 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
126
  - lib/alchemy-dragonfly-s3.rb
136
127
  - lib/alchemy/attachment_monkey_patch.rb
137
128
  - lib/alchemy/dragonfly/s3/engine.rb
@@ -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 < BaseRecord
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 < BaseRecord
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 < BaseRecord
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 < BaseRecord
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 < BaseRecord
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 < BaseRecord
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,54 +0,0 @@
1
- <div class="resource_info">
2
- <div class="value">
3
- <label>
4
- <%= render_icon @attachment.icon_css_class, style: 'regular', size: 'lg' %>
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, style: 'regular') %>
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, style: 'regular') %>
20
- </a>
21
- </div>
22
- </div>
23
-
24
- <% case @attachment.icon_css_class %>
25
- <% when "file-image" %>
26
- <div class="attachment_preview_container image-preview">
27
- <%= image_tag(@attachment.url, class: "full_width") %>
28
- </div>
29
- <% when "file-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 "file-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 "file-pdf" %>
38
- <iframe src="<%= @attachment.url %>" frameborder=0 class="full-iframe">
39
- Your browser does not support frames.
40
- </iframe>
41
- <% end %>
42
-
43
- <script type="text/javascript">
44
- $(function() {
45
- var clipboard = new Clipboard('.icon_button--right');
46
- clipboard.on('success', function(e) {
47
- Alchemy.growl('<%= Alchemy.t("Copied to clipboard") %>');
48
- e.clearSelection();
49
- });
50
- Alchemy.currentDialog().dialog.on('DialogClose.Alchemy', function() {
51
- clipboard.destroy();
52
- });
53
- });
54
- </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
- render_icon(:minus),
9
- Alchemy.t(:confirm_to_delete_image_from_server),
10
- alchemy.admin_picture_path(
11
- id: picture,
12
- q: search_filter_params[:q],
13
- page: params[:page],
14
- tagged_with: search_filter_params[:tagged_with],
15
- size: params[:size],
16
- filter: search_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: search_filter_params[:q],
35
- page: params[:page],
36
- tagged_with: search_filter_params[:tagged_with],
37
- size: params[:size],
38
- filter: search_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,21 +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
- <%= link_to(
3
- image_tag(
4
- picture_to_assign.url(size: preview_size(size), flatten: true) || "alchemy/missing-image.svg",
5
- alt: picture_to_assign.name
6
- ),
7
- alchemy.assign_admin_essence_pictures_path(
8
- picture_id: picture_to_assign.id,
9
- content_id: @content,
10
- options: options
11
- ),
12
- remote: true,
13
- onclick: '$(self).attr("href", "#").off("click"); return false',
14
- method: 'put',
15
- title: Alchemy.t(:assign_image),
16
- class: 'thumbnail_background'
17
- ) %>
18
- <div class="picture_name" title="<%= picture_to_assign.name %>">
19
- <%= picture_to_assign.name %>
20
- </div>
21
- </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: search_filter_params[:q],
10
- page: params[:page],
11
- tagged_with: search_filter_params[:tagged_with],
12
- size: params[:size],
13
- filter: search_filter_params[:filter]
14
- ),
15
- class: "previous-picture",
16
- remote: true do %>
17
- <i class="icon fas fa-angle-left fa-fw"></i>
18
- <% end %>
19
- <% end %>
20
- <% if @next %>
21
- <%= link_to alchemy.admin_picture_path(
22
- id: @next,
23
- q: search_filter_params[:q],
24
- page: params[:page],
25
- tagged_with: search_filter_params[:tagged_with],
26
- size: params[:size],
27
- filter: search_filter_params[:filter]
28
- ),
29
- class: "next-picture",
30
- remote: true do %>
31
- <i class="icon fas fa-angle-right fa-fw"></i>
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
- <i class="icon fas fa-angle-double-right fa-fw"></i>
43
- </div>
@@ -1,14 +0,0 @@
1
- <% content = local_assigns[:content] || local_assigns[:essence_file_view] %>
2
- <%- if attachment = content.ingredient -%>
3
- <%- html_options = local_assigns.fetch(:html_options, {}) -%>
4
- <%= link_to(
5
- content.essence.link_text.presence ||
6
- content.settings_value(:link_text, local_assigns.fetch(:options, {})) ||
7
- attachment.name,
8
- attachment.url,
9
- {
10
- class: content.essence.css_class.presence,
11
- title: content.essence.title.presence
12
- }.merge(html_options)
13
- ) -%>
14
- <%- end -%>
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class CreateAlchemyPictureThumbs < ActiveRecord::Migration[5.0]
4
- def change
5
- create_table :alchemy_picture_thumbs do |t|
6
- t.references :picture, null: false, foreign_key: { to_table: :alchemy_pictures }
7
- t.string :signature, null: false
8
- t.text :uid, null: false
9
- end
10
- add_index :alchemy_picture_thumbs, :signature, unique: true
11
- end
12
- end