paperweight 0.0.1
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 +7 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +4 -0
- data/.travis.yml +11 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +186 -0
- data/LICENSE +21 -0
- data/README.md +98 -0
- data/Rakefile +13 -0
- data/bin/console +8 -0
- data/bin/setup +6 -0
- data/lib/paperweight.rb +19 -0
- data/lib/paperweight/config.rb +24 -0
- data/lib/paperweight/download.rb +91 -0
- data/lib/paperweight/image.rb +61 -0
- data/lib/paperweight/location.rb +40 -0
- data/lib/paperweight/purge_job.rb +25 -0
- data/lib/paperweight/railtie.rb +70 -0
- data/lib/paperweight/storage.rb +65 -0
- data/lib/paperweight/thumbnails_job.rb +14 -0
- data/lib/paperweight/version.rb +5 -0
- data/paperweight.gemspec +34 -0
- metadata +191 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a6caebfa164c20badafc768b093809531a371e1160511abf7ba0fc59e9c388c8
|
|
4
|
+
data.tar.gz: 80639d98c7b691f94d021a042809c528756a140bfa9e6c80bf6027664bc5807e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '03438392a5e8c5159029531f8601d9a8c93ed8b08da3f6b2cc6f5b8cd330865a58b5df5ef1cd10d020aed8042780ca9221d47cd7a7075f2237166c810f0419e5'
|
|
7
|
+
data.tar.gz: d301837a0e970f3be15d2caf2ab5d2174e8e36c7db515b2cb497fb2340a045b178db7abb2ba7a7dbc875bf25e774ae623d1a1992348431e3a735862b54e5ebd8
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
paperweight (0.0.1)
|
|
5
|
+
aws-sdk-s3 (~> 1.13)
|
|
6
|
+
paperclip (~> 6.0)
|
|
7
|
+
rails (~> 5.2)
|
|
8
|
+
|
|
9
|
+
GEM
|
|
10
|
+
remote: https://rubygems.org/
|
|
11
|
+
specs:
|
|
12
|
+
actioncable (5.2.0)
|
|
13
|
+
actionpack (= 5.2.0)
|
|
14
|
+
nio4r (~> 2.0)
|
|
15
|
+
websocket-driver (>= 0.6.1)
|
|
16
|
+
actionmailer (5.2.0)
|
|
17
|
+
actionpack (= 5.2.0)
|
|
18
|
+
actionview (= 5.2.0)
|
|
19
|
+
activejob (= 5.2.0)
|
|
20
|
+
mail (~> 2.5, >= 2.5.4)
|
|
21
|
+
rails-dom-testing (~> 2.0)
|
|
22
|
+
actionpack (5.2.0)
|
|
23
|
+
actionview (= 5.2.0)
|
|
24
|
+
activesupport (= 5.2.0)
|
|
25
|
+
rack (~> 2.0)
|
|
26
|
+
rack-test (>= 0.6.3)
|
|
27
|
+
rails-dom-testing (~> 2.0)
|
|
28
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
|
29
|
+
actionview (5.2.0)
|
|
30
|
+
activesupport (= 5.2.0)
|
|
31
|
+
builder (~> 3.1)
|
|
32
|
+
erubi (~> 1.4)
|
|
33
|
+
rails-dom-testing (~> 2.0)
|
|
34
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
|
35
|
+
activejob (5.2.0)
|
|
36
|
+
activesupport (= 5.2.0)
|
|
37
|
+
globalid (>= 0.3.6)
|
|
38
|
+
activemodel (5.2.0)
|
|
39
|
+
activesupport (= 5.2.0)
|
|
40
|
+
activerecord (5.2.0)
|
|
41
|
+
activemodel (= 5.2.0)
|
|
42
|
+
activesupport (= 5.2.0)
|
|
43
|
+
arel (>= 9.0)
|
|
44
|
+
activestorage (5.2.0)
|
|
45
|
+
actionpack (= 5.2.0)
|
|
46
|
+
activerecord (= 5.2.0)
|
|
47
|
+
marcel (~> 0.3.1)
|
|
48
|
+
activesupport (5.2.0)
|
|
49
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
50
|
+
i18n (>= 0.7, < 2)
|
|
51
|
+
minitest (~> 5.1)
|
|
52
|
+
tzinfo (~> 1.1)
|
|
53
|
+
arel (9.0.0)
|
|
54
|
+
ast (2.4.0)
|
|
55
|
+
aws-eventstream (1.0.0)
|
|
56
|
+
aws-partitions (1.91.0)
|
|
57
|
+
aws-sdk-core (3.21.2)
|
|
58
|
+
aws-eventstream (~> 1.0)
|
|
59
|
+
aws-partitions (~> 1.0)
|
|
60
|
+
aws-sigv4 (~> 1.0)
|
|
61
|
+
jmespath (~> 1.0)
|
|
62
|
+
aws-sdk-kms (1.5.0)
|
|
63
|
+
aws-sdk-core (~> 3)
|
|
64
|
+
aws-sigv4 (~> 1.0)
|
|
65
|
+
aws-sdk-s3 (1.13.0)
|
|
66
|
+
aws-sdk-core (~> 3, >= 3.21.2)
|
|
67
|
+
aws-sdk-kms (~> 1)
|
|
68
|
+
aws-sigv4 (~> 1.0)
|
|
69
|
+
aws-sigv4 (1.0.2)
|
|
70
|
+
builder (3.2.3)
|
|
71
|
+
climate_control (0.2.0)
|
|
72
|
+
concurrent-ruby (1.0.5)
|
|
73
|
+
crass (1.0.4)
|
|
74
|
+
docile (1.3.1)
|
|
75
|
+
erubi (1.7.1)
|
|
76
|
+
globalid (0.4.1)
|
|
77
|
+
activesupport (>= 4.2.0)
|
|
78
|
+
i18n (1.0.1)
|
|
79
|
+
concurrent-ruby (~> 1.0)
|
|
80
|
+
jaro_winkler (1.5.1)
|
|
81
|
+
jmespath (1.4.0)
|
|
82
|
+
json (2.1.0)
|
|
83
|
+
loofah (2.2.2)
|
|
84
|
+
crass (~> 1.0.2)
|
|
85
|
+
nokogiri (>= 1.5.9)
|
|
86
|
+
mail (2.7.0)
|
|
87
|
+
mini_mime (>= 0.1.1)
|
|
88
|
+
marcel (0.3.2)
|
|
89
|
+
mimemagic (~> 0.3.2)
|
|
90
|
+
method_source (0.9.0)
|
|
91
|
+
mime-types (3.1)
|
|
92
|
+
mime-types-data (~> 3.2015)
|
|
93
|
+
mime-types-data (3.2016.0521)
|
|
94
|
+
mimemagic (0.3.2)
|
|
95
|
+
mini_mime (1.0.0)
|
|
96
|
+
mini_portile2 (2.3.0)
|
|
97
|
+
minitest (5.11.3)
|
|
98
|
+
nio4r (2.3.1)
|
|
99
|
+
nokogiri (1.8.2)
|
|
100
|
+
mini_portile2 (~> 2.3.0)
|
|
101
|
+
paperclip (6.0.0)
|
|
102
|
+
activemodel (>= 4.2.0)
|
|
103
|
+
activesupport (>= 4.2.0)
|
|
104
|
+
mime-types
|
|
105
|
+
mimemagic (~> 0.3.0)
|
|
106
|
+
terrapin (~> 0.6.0)
|
|
107
|
+
parallel (1.12.1)
|
|
108
|
+
parser (2.5.1.0)
|
|
109
|
+
ast (~> 2.4.0)
|
|
110
|
+
powerpack (0.1.2)
|
|
111
|
+
rack (2.0.5)
|
|
112
|
+
rack-test (1.0.0)
|
|
113
|
+
rack (>= 1.0, < 3)
|
|
114
|
+
rails (5.2.0)
|
|
115
|
+
actioncable (= 5.2.0)
|
|
116
|
+
actionmailer (= 5.2.0)
|
|
117
|
+
actionpack (= 5.2.0)
|
|
118
|
+
actionview (= 5.2.0)
|
|
119
|
+
activejob (= 5.2.0)
|
|
120
|
+
activemodel (= 5.2.0)
|
|
121
|
+
activerecord (= 5.2.0)
|
|
122
|
+
activestorage (= 5.2.0)
|
|
123
|
+
activesupport (= 5.2.0)
|
|
124
|
+
bundler (>= 1.3.0)
|
|
125
|
+
railties (= 5.2.0)
|
|
126
|
+
sprockets-rails (>= 2.0.0)
|
|
127
|
+
rails-dom-testing (2.0.3)
|
|
128
|
+
activesupport (>= 4.2.0)
|
|
129
|
+
nokogiri (>= 1.6)
|
|
130
|
+
rails-html-sanitizer (1.0.4)
|
|
131
|
+
loofah (~> 2.2, >= 2.2.2)
|
|
132
|
+
railties (5.2.0)
|
|
133
|
+
actionpack (= 5.2.0)
|
|
134
|
+
activesupport (= 5.2.0)
|
|
135
|
+
method_source
|
|
136
|
+
rake (>= 0.8.7)
|
|
137
|
+
thor (>= 0.18.1, < 2.0)
|
|
138
|
+
rainbow (3.0.0)
|
|
139
|
+
rake (12.3.1)
|
|
140
|
+
rubocop (0.57.2)
|
|
141
|
+
jaro_winkler (~> 1.5.1)
|
|
142
|
+
parallel (~> 1.10)
|
|
143
|
+
parser (>= 2.5)
|
|
144
|
+
powerpack (~> 0.1)
|
|
145
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
146
|
+
ruby-progressbar (~> 1.7)
|
|
147
|
+
unicode-display_width (~> 1.0, >= 1.0.1)
|
|
148
|
+
ruby-progressbar (1.9.0)
|
|
149
|
+
simplecov (0.16.1)
|
|
150
|
+
docile (~> 1.1)
|
|
151
|
+
json (>= 1.8, < 3)
|
|
152
|
+
simplecov-html (~> 0.10.0)
|
|
153
|
+
simplecov-html (0.10.2)
|
|
154
|
+
sprockets (3.7.1)
|
|
155
|
+
concurrent-ruby (~> 1.0)
|
|
156
|
+
rack (> 1, < 3)
|
|
157
|
+
sprockets-rails (3.2.1)
|
|
158
|
+
actionpack (>= 4.0)
|
|
159
|
+
activesupport (>= 4.0)
|
|
160
|
+
sprockets (>= 3.0.0)
|
|
161
|
+
sqlite3 (1.3.13)
|
|
162
|
+
terrapin (0.6.0)
|
|
163
|
+
climate_control (>= 0.0.3, < 1.0)
|
|
164
|
+
thor (0.20.0)
|
|
165
|
+
thread_safe (0.3.6)
|
|
166
|
+
tzinfo (1.2.5)
|
|
167
|
+
thread_safe (~> 0.1)
|
|
168
|
+
unicode-display_width (1.4.0)
|
|
169
|
+
websocket-driver (0.7.0)
|
|
170
|
+
websocket-extensions (>= 0.1.0)
|
|
171
|
+
websocket-extensions (0.1.3)
|
|
172
|
+
|
|
173
|
+
PLATFORMS
|
|
174
|
+
ruby
|
|
175
|
+
|
|
176
|
+
DEPENDENCIES
|
|
177
|
+
bundler (~> 1.16)
|
|
178
|
+
minitest (~> 5.11)
|
|
179
|
+
paperweight!
|
|
180
|
+
rake (~> 12.0)
|
|
181
|
+
rubocop (~> 0.57)
|
|
182
|
+
simplecov (~> 0.16)
|
|
183
|
+
sqlite3 (~> 1.3)
|
|
184
|
+
|
|
185
|
+
BUNDLED WITH
|
|
186
|
+
1.16.1
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 CultureHQ
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Paperweight
|
|
2
|
+
|
|
3
|
+
[](https://travis-ci.com/CultureHQ/paperweight)
|
|
4
|
+
|
|
5
|
+
An opinionated Paperclip for a specific workflow. Only accepts image URLs instead of uploaded files. The image URLs are then queued for downloading, converting, and generating thumbnails in the background using `ActiveJob`.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'paperweight'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And then execute:
|
|
16
|
+
|
|
17
|
+
$ bundle
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
|
|
21
|
+
$ gem install paperweight
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
First, configure `paperweight` in an initializer, e.g., `config/initializers/paperweight.rb`:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
Paperweight.configure do |config|
|
|
29
|
+
# your asset server (preferably a CDN)
|
|
30
|
+
config.asset_server = 'https://uploads.culturehq.com'
|
|
31
|
+
|
|
32
|
+
# the S3 bucket to which to uploads images
|
|
33
|
+
config.bucket = 'culturehq-uploads'
|
|
34
|
+
|
|
35
|
+
# the AWS credentials used with permission to S3
|
|
36
|
+
config.credentials = {
|
|
37
|
+
access_key_id: Rails.application.credentials.aws_access_key_id,
|
|
38
|
+
secret_access_key: Rails.application.credentials.aws_secret_access_key,
|
|
39
|
+
region: 'us-west-2'
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then, create and run a migration to add the `image_uuid` and `image_processing` columns to your model:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
class AddImageToPost < ActiveRecord::Migration[5.2]
|
|
48
|
+
def change
|
|
49
|
+
add_column :posts, :image_uuid, :string
|
|
50
|
+
add_column :posts, :image_processing, :boolean, default: false, null: false
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Next, add the class macro to the model:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
class Post < ApplicationRecord
|
|
59
|
+
has_image thumb: '100x100>'
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Finally, add the ability to update the `image_url` attribute from the controller:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
class PostsController < ApplicationController
|
|
67
|
+
# PUT|PATCH /posts/:id
|
|
68
|
+
def update
|
|
69
|
+
post = Post.find(params[:id])
|
|
70
|
+
|
|
71
|
+
if post.update(post_params)
|
|
72
|
+
render json: post
|
|
73
|
+
else
|
|
74
|
+
render error: post.error, status: :unprocessable_entity
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def post_params
|
|
81
|
+
params.permit(:image_url)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Development
|
|
87
|
+
|
|
88
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
89
|
+
|
|
90
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
91
|
+
|
|
92
|
+
## Contributing
|
|
93
|
+
|
|
94
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/CultureHQ/paperweight.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rake/testtask'
|
|
5
|
+
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << 'test'
|
|
8
|
+
t.libs << 'lib'
|
|
9
|
+
t.test_files = FileList['test/**/*_test.rb']
|
|
10
|
+
t.warning = false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
task default: :test
|
data/bin/console
ADDED
data/bin/setup
ADDED
data/lib/paperweight.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aws-sdk-s3'
|
|
4
|
+
require 'open-uri'
|
|
5
|
+
require 'net/http'
|
|
6
|
+
require 'paperclip'
|
|
7
|
+
require 'rails'
|
|
8
|
+
|
|
9
|
+
require 'active_job'
|
|
10
|
+
|
|
11
|
+
require 'paperweight/config'
|
|
12
|
+
require 'paperweight/download'
|
|
13
|
+
require 'paperweight/image'
|
|
14
|
+
require 'paperweight/location'
|
|
15
|
+
require 'paperweight/purge_job'
|
|
16
|
+
require 'paperweight/railtie'
|
|
17
|
+
require 'paperweight/storage'
|
|
18
|
+
require 'paperweight/thumbnails_job'
|
|
19
|
+
require 'paperweight/version'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# An opinionated Paperclip
|
|
4
|
+
module Paperweight
|
|
5
|
+
# Stores various metadata about the configuration of `paperweight`.
|
|
6
|
+
class Config
|
|
7
|
+
attr_accessor :asset_server, :bucket, :credentials
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@bucket = ''
|
|
11
|
+
@credentials = { region: 'us-east-1' }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def config
|
|
17
|
+
@config ||= Config.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def configure
|
|
21
|
+
yield config
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Paperweight
|
|
4
|
+
# Most of this from https://gist.github.com/janko-m/7cd94b8b4dd113c2c193
|
|
5
|
+
class Download
|
|
6
|
+
Error = Class.new(StandardError)
|
|
7
|
+
|
|
8
|
+
DOWNLOAD_ERRORS = [
|
|
9
|
+
SocketError, # domain not found
|
|
10
|
+
OpenURI::HTTPError, # response status 4xx or 5xx
|
|
11
|
+
RuntimeError, # redirection errors (e.g. redirection loop)
|
|
12
|
+
URI::InvalidURIError, # invalid URL
|
|
13
|
+
Error # our errors
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
MAX_SIZE = 10 * 1024 * 1024
|
|
17
|
+
|
|
18
|
+
def download(url)
|
|
19
|
+
# Finally we download the file. Here we mustn't use simple #open that
|
|
20
|
+
# open-uri overrides, because this is vulnerable to shell execution
|
|
21
|
+
# attack (if #open method detects a starting pipe (e.g. "| ls"), it will
|
|
22
|
+
# execute the following as a shell command).
|
|
23
|
+
normalize_download(uri_from(url).open(open_options))
|
|
24
|
+
rescue *DOWNLOAD_ERRORS => error
|
|
25
|
+
message = error.message
|
|
26
|
+
|
|
27
|
+
# open-uri will throw a RuntimeError when it detects a redirection loop,
|
|
28
|
+
# so we want to reraise the exception if it was some other RuntimeError
|
|
29
|
+
raise if error.is_a?(RuntimeError) && message !~ /redirection/
|
|
30
|
+
|
|
31
|
+
# We raise our unified Error class
|
|
32
|
+
raise Error, "download failed (#{url}): #{message}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.download(url)
|
|
36
|
+
new.download(url)
|
|
37
|
+
rescue Error
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# open-uri will return a StringIO instead of a Tempfile if the filesize
|
|
44
|
+
# is less than 10 KB, so we patch this behaviour by converting it into a
|
|
45
|
+
# Tempfile.
|
|
46
|
+
def normalize_download(file)
|
|
47
|
+
return file unless file.is_a?(StringIO)
|
|
48
|
+
|
|
49
|
+
# We need to open it in binary mode for Windows users.
|
|
50
|
+
Tempfile.new('download-', binmode: true).tap do |tempfile|
|
|
51
|
+
# IO.copy_stream is the most efficient way of data transfer.
|
|
52
|
+
IO.copy_stream(file, tempfile.path)
|
|
53
|
+
|
|
54
|
+
# We add the metadata that open-uri puts on the file
|
|
55
|
+
# (e.g. #content_type)
|
|
56
|
+
OpenURI::Meta.init(tempfile)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def open_options
|
|
61
|
+
{}.tap do |options|
|
|
62
|
+
# It was shown that in a random sample approximately 20% of websites
|
|
63
|
+
# will simply refuse a request which doesn't have a valid User-Agent.
|
|
64
|
+
options['User-Agent'] = 'Paperweight'
|
|
65
|
+
|
|
66
|
+
# It's good to shield ourselves from files that are too big. open-uri
|
|
67
|
+
# will call this block as soon as it gets the "Content-Length" header,
|
|
68
|
+
# which means that we can bail out before we download the file.
|
|
69
|
+
options[:content_length_proc] = lambda { |size|
|
|
70
|
+
if size && size > MAX_SIZE
|
|
71
|
+
raise Error, "file is too big (max is #{MAX_SIZE})"
|
|
72
|
+
end
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Disabling :reek:ManualDispatch here because we don't control the URI API
|
|
78
|
+
def uri_from(url)
|
|
79
|
+
# This will raise an InvalidURIError if the URL is very wrong. It will
|
|
80
|
+
# still pass for strings like "foo", though.
|
|
81
|
+
url = URI(url)
|
|
82
|
+
|
|
83
|
+
# We need to check if the URL was either http://, https:// or ftp://,
|
|
84
|
+
# because these are the only ones we can download from. open-uri will add
|
|
85
|
+
# the #open method only to these ones, so this is a good check.
|
|
86
|
+
raise Error, 'url was invalid' unless url.respond_to?(:open)
|
|
87
|
+
|
|
88
|
+
url
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Paperweight
|
|
4
|
+
# The image object attached to the model.
|
|
5
|
+
class Image
|
|
6
|
+
attr_reader :model
|
|
7
|
+
|
|
8
|
+
def initialize(model)
|
|
9
|
+
@model = model
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def as_json(*)
|
|
13
|
+
serialized_styles_for(model).merge!(
|
|
14
|
+
is_default: model.image? ? false : true,
|
|
15
|
+
processing: model.image_processing?
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def update_file(file)
|
|
20
|
+
image_styles.each do |style, geometry|
|
|
21
|
+
options = { geometry: geometry, convert_options: '-strip' }
|
|
22
|
+
thumbnail = Paperclip::Thumbnail.new(file, options).make
|
|
23
|
+
Storage.adapter.put(thumbnail, Location.new(model, style).path)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
model.clear_image_url
|
|
27
|
+
model.update!(image_processing: false)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def update_url(url)
|
|
31
|
+
update_file(Download.download(url))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def path(style = :original)
|
|
35
|
+
Location.new(model, style).path
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def url(style = :original)
|
|
39
|
+
Location.new(model, style).url
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def image_styles
|
|
45
|
+
model.class.image_styles
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def serialized_styles_for(model)
|
|
49
|
+
url_method = url_method_for(model)
|
|
50
|
+
|
|
51
|
+
image_styles.each_key.each_with_object({}) do |style, serialized|
|
|
52
|
+
serialized[:"#{style}_url"] =
|
|
53
|
+
Location.new(model, style).public_send(url_method)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def url_method_for(model)
|
|
58
|
+
!model.image? || model.image_processing? ? :default_url : :url
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Paperweight
|
|
4
|
+
# The location of the attachment in terms of paths are URLs.
|
|
5
|
+
class Location
|
|
6
|
+
attr_reader :model, :style
|
|
7
|
+
|
|
8
|
+
def initialize(model, style = :original)
|
|
9
|
+
@model = model
|
|
10
|
+
@style = style
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def path
|
|
14
|
+
"#{table_name}/#{model.id}-#{style}-#{model.image_uuid}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def url
|
|
18
|
+
"#{self.class.prefix}/#{path}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def default_url
|
|
22
|
+
"#{self.class.prefix}/assets/#{table_name}/#{style}.gif"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.prefix
|
|
26
|
+
@prefix ||=
|
|
27
|
+
if Rails.env.production?
|
|
28
|
+
Paperweight.config.asset_server
|
|
29
|
+
else
|
|
30
|
+
'http://localhost:3000/uploads'
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def table_name
|
|
37
|
+
model.class.table_name
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Paperweight
|
|
4
|
+
# Removes all previous image files matching the given pattern.
|
|
5
|
+
class PurgeJob < ActiveJob::Base
|
|
6
|
+
PLACEHOLDER = ':style'
|
|
7
|
+
|
|
8
|
+
queue_as :default
|
|
9
|
+
|
|
10
|
+
def perform(pattern, styles)
|
|
11
|
+
styles.each do |style|
|
|
12
|
+
Storage.adapter.delete(pattern.gsub(PLACEHOLDER, style))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.perform_later_for(model, image_uuid = nil)
|
|
17
|
+
model.image_uuid = image_uuid if image_uuid
|
|
18
|
+
|
|
19
|
+
perform_later(
|
|
20
|
+
Location.new(model, PLACEHOLDER).path,
|
|
21
|
+
model.class.image_styles.keys.map(&:to_s)
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Paperweight
|
|
4
|
+
# Adds the `paperweight` hook in ActiveRecord models.
|
|
5
|
+
class Railtie < Rails::Railtie
|
|
6
|
+
# Extensions that need be generated from input.
|
|
7
|
+
class DynamicExtension < Module
|
|
8
|
+
def initialize(styles)
|
|
9
|
+
define_method(:image_styles) { styles }
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Extensions that are the same for each kind of attachment.
|
|
14
|
+
module StaticExtension
|
|
15
|
+
def clear_image_url
|
|
16
|
+
@image_url = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def image
|
|
20
|
+
Image.new(self)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def image?
|
|
24
|
+
image_uuid
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def image_url
|
|
28
|
+
@image_url
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def image_url=(url)
|
|
32
|
+
assign_attributes(
|
|
33
|
+
image_processing: url ? true : false,
|
|
34
|
+
image_uuid: url ? SecureRandom.uuid : nil
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@image_url = url
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.included(base)
|
|
41
|
+
base.after_commit do
|
|
42
|
+
PurgeJob.perform_later_for(self, prev_image_uuid) if prev_image_uuid
|
|
43
|
+
ThumbnailsJob.perform_later(self, image_url) if image_url
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
base.before_destroy if: :image? do
|
|
47
|
+
PurgeJob.perform_later_for(self)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def prev_image_uuid
|
|
54
|
+
previous_changes.fetch('image_uuid', [])[0]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Provides the `has_image` method to attach to models.
|
|
59
|
+
module Hook
|
|
60
|
+
def has_image(styles = {}) # rubocop:disable Naming/PredicateName
|
|
61
|
+
extend DynamicExtension.new({ original: '' }.merge!(styles))
|
|
62
|
+
include StaticExtension
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
initializer 'paperweight' do
|
|
67
|
+
ActiveSupport.on_load(:active_record) { extend Hook }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Paperweight
|
|
4
|
+
# Handles putting and deleting files from different kinds of storage.
|
|
5
|
+
module Storage
|
|
6
|
+
# A storage adapter for Amazon AWS S3.
|
|
7
|
+
class Remote
|
|
8
|
+
attr_reader :client
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@client = ::Aws::S3::Client.new(Paperweight.config.credentials)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def delete(key)
|
|
15
|
+
client.delete_object(bucket: Paperweight.config.bucket, key: key)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def put(file, key)
|
|
19
|
+
client.put_object(
|
|
20
|
+
bucket: Paperweight.config.bucket,
|
|
21
|
+
key: key,
|
|
22
|
+
content_type: Paperclip::ContentTypeDetector.new(file.path).detect,
|
|
23
|
+
content_disposition: 'attachment',
|
|
24
|
+
body: file
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# A storage adapter for a local filesystem.
|
|
30
|
+
class Local
|
|
31
|
+
attr_reader :root
|
|
32
|
+
|
|
33
|
+
def initialize(root)
|
|
34
|
+
@root = root
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def delete(key)
|
|
38
|
+
FileUtils.rm(File.join(root, key))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def put(file, key)
|
|
42
|
+
filepath = File.join(root, key)
|
|
43
|
+
FileUtils.mkdir_p(File.dirname(filepath))
|
|
44
|
+
FileUtils.cp(file.path, filepath)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
def adapter
|
|
50
|
+
@adapter ||=
|
|
51
|
+
case Rails.env
|
|
52
|
+
when 'production' then Remote.new
|
|
53
|
+
when 'development' then local_for('public')
|
|
54
|
+
when 'test' then local_for('tmp')
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def local_for(directory)
|
|
61
|
+
Local.new(Rails.root.join(directory, 'uploads').to_s.freeze)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Paperweight
|
|
4
|
+
# Queues thumbnails for processing.
|
|
5
|
+
class ThumbnailsJob < ActiveJob::Base
|
|
6
|
+
queue_as :default
|
|
7
|
+
|
|
8
|
+
discard_on ActiveJob::DeserializationError
|
|
9
|
+
|
|
10
|
+
def perform(model, url)
|
|
11
|
+
model.image.update_url(url)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/paperweight.gemspec
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
+
require 'paperweight/version'
|
|
6
|
+
|
|
7
|
+
Gem::Specification.new do |spec|
|
|
8
|
+
spec.name = 'paperweight'
|
|
9
|
+
spec.version = Paperweight::VERSION
|
|
10
|
+
spec.authors = ['Kevin Deisz']
|
|
11
|
+
spec.email = ['kevin.deisz@gmail.com']
|
|
12
|
+
|
|
13
|
+
spec.summary = 'An opinionated Paperclip.'
|
|
14
|
+
spec.homepage = 'https://github.com/CultureHQ/paperweight'
|
|
15
|
+
spec.license = 'MIT'
|
|
16
|
+
|
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
|
19
|
+
end
|
|
20
|
+
spec.bindir = 'exe'
|
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
22
|
+
spec.require_paths = ['lib']
|
|
23
|
+
|
|
24
|
+
spec.add_dependency 'aws-sdk-s3', '~> 1.13'
|
|
25
|
+
spec.add_dependency 'paperclip', '~> 6.0'
|
|
26
|
+
spec.add_dependency 'rails', '~> 5.2'
|
|
27
|
+
|
|
28
|
+
spec.add_development_dependency 'bundler', '~> 1.16'
|
|
29
|
+
spec.add_development_dependency 'minitest', '~> 5.11'
|
|
30
|
+
spec.add_development_dependency 'rake', '~> 12.0'
|
|
31
|
+
spec.add_development_dependency 'rubocop', '~> 0.57'
|
|
32
|
+
spec.add_development_dependency 'simplecov', '~> 0.16'
|
|
33
|
+
spec.add_development_dependency 'sqlite3', '~> 1.3'
|
|
34
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: paperweight
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kevin Deisz
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2018-06-14 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: aws-sdk-s3
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.13'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.13'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: paperclip
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '6.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '6.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rails
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '5.2'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '5.2'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: bundler
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.16'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.16'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: minitest
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '5.11'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '5.11'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rake
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '12.0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '12.0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rubocop
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0.57'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0.57'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: simplecov
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0.16'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0.16'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: sqlite3
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '1.3'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '1.3'
|
|
139
|
+
description:
|
|
140
|
+
email:
|
|
141
|
+
- kevin.deisz@gmail.com
|
|
142
|
+
executables: []
|
|
143
|
+
extensions: []
|
|
144
|
+
extra_rdoc_files: []
|
|
145
|
+
files:
|
|
146
|
+
- ".gitignore"
|
|
147
|
+
- ".rubocop.yml"
|
|
148
|
+
- ".travis.yml"
|
|
149
|
+
- Gemfile
|
|
150
|
+
- Gemfile.lock
|
|
151
|
+
- LICENSE
|
|
152
|
+
- README.md
|
|
153
|
+
- Rakefile
|
|
154
|
+
- bin/console
|
|
155
|
+
- bin/setup
|
|
156
|
+
- lib/paperweight.rb
|
|
157
|
+
- lib/paperweight/config.rb
|
|
158
|
+
- lib/paperweight/download.rb
|
|
159
|
+
- lib/paperweight/image.rb
|
|
160
|
+
- lib/paperweight/location.rb
|
|
161
|
+
- lib/paperweight/purge_job.rb
|
|
162
|
+
- lib/paperweight/railtie.rb
|
|
163
|
+
- lib/paperweight/storage.rb
|
|
164
|
+
- lib/paperweight/thumbnails_job.rb
|
|
165
|
+
- lib/paperweight/version.rb
|
|
166
|
+
- paperweight.gemspec
|
|
167
|
+
homepage: https://github.com/CultureHQ/paperweight
|
|
168
|
+
licenses:
|
|
169
|
+
- MIT
|
|
170
|
+
metadata: {}
|
|
171
|
+
post_install_message:
|
|
172
|
+
rdoc_options: []
|
|
173
|
+
require_paths:
|
|
174
|
+
- lib
|
|
175
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
176
|
+
requirements:
|
|
177
|
+
- - ">="
|
|
178
|
+
- !ruby/object:Gem::Version
|
|
179
|
+
version: '0'
|
|
180
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
181
|
+
requirements:
|
|
182
|
+
- - ">="
|
|
183
|
+
- !ruby/object:Gem::Version
|
|
184
|
+
version: '0'
|
|
185
|
+
requirements: []
|
|
186
|
+
rubyforge_project:
|
|
187
|
+
rubygems_version: 2.7.6
|
|
188
|
+
signing_key:
|
|
189
|
+
specification_version: 4
|
|
190
|
+
summary: An opinionated Paperclip.
|
|
191
|
+
test_files: []
|