├── .gitignore
├── CHANGES.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── lib
├── paperdragon.rb
└── paperdragon
│ ├── attachment.rb
│ ├── file.rb
│ ├── file
│ └── operations.rb
│ ├── metadata.rb
│ ├── model.rb
│ ├── paperclip.rb
│ ├── paperclip
│ └── model.rb
│ ├── task.rb
│ └── version.rb
├── paperdragon.gemspec
└── test
├── attachment_test.rb
├── file_test.rb
├── fixtures
├── apotomo.png
└── trb.png
├── metadata_test.rb
├── model_test.rb
├── paperclip_uid_test.rb
├── task_test.rb
└── test_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | .bundle
4 | .config
5 | .yardoc
6 | Gemfile.lock
7 | InstalledFiles
8 | _yardoc
9 | coverage
10 | doc/
11 | lib/bundler/man
12 | pkg
13 | rdoc
14 | spec/reports
15 | test/tmp
16 | test/version_tmp
17 | tmp
18 | *.bundle
19 | *.so
20 | *.o
21 | *.a
22 | mkmf.log
23 | /public
24 | /dragonfly.log
25 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | # 0.0.11
2 |
3 | * You can now have any name for attachments in `Model`. This also allows having multiple uploads per model. E.g. the following now works.
4 |
5 | ```ruby
6 | processable :avatar, Attachment
7 | processable :image, Attachment
8 | ```
9 |
10 | You will need `avatar_meta_data` and `image_meta_data` fields, now.
11 |
12 | Many thanks to @mrbongiolo for implementing this.
13 |
14 | # 0.0.10
15 |
16 | * Require Dragonfly 1.0.12 or above and change internal API accordingly. Thanks @acaron for fixing that!
17 |
18 | # 0.0.9
19 |
20 | * Add `Task#delete!` which allows to delete files in task blocks.
21 |
22 | # 0.0.8
23 |
24 | * Introduce `Model::Writer` and `Model::Reader` in case you don't like `Model#image`'s fuzzy API.
25 |
26 | # 0.0.7
27 |
28 | * `Task#process!` (and the delegated `File#process!`) now delete the original version of an attachment if `process!` is used to replace the latter.
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in paperdragon.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 Nick Sutterer
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Paperdragon
2 |
3 | _Explicit image processing._
4 |
5 | ## Summary
6 |
7 | Paperdragon gives you image processing as known from [Paperclip](https://github.com/thoughtbot/paperclip), [CarrierWave](https://github.com/carrierwaveuploader/carrierwave) or [Dragonfly](https://github.com/markevans/dragonfly). It allows uploading, cropping, resizing, watermarking, maintaining different versions of an image, and so on.
8 |
9 | It provides a very explicit DSL: **No magic is happening behind the scenes, paperdragon makes _you_ implement the processing steps.**
10 |
11 | With only a little bit of more code you are fully in control of what gets uploaded where, which image version gets resized when and what gets sent to a background job.
12 |
13 | Paperdragon uses the excellent [Dragonfly](https://github.com/markevans/dragonfly) gem for processing, resizing, storing, etc.
14 |
15 | Paperdragon is database-agnostic, doesn't know anything about ActiveRecord and _does not_ hook into AR's callbacks.
16 |
17 | ## Installation
18 |
19 | Add this line to your application's Gemfile:
20 |
21 | ```ruby
22 | gem 'paperdragon'
23 | ```
24 |
25 |
26 | ## Example
27 |
28 | This README only documents the public DSL. You're free to use the public API [documented here](# TODO) if you don't like the DSL.
29 |
30 | ### Model
31 |
32 | Paperdragon has only one requirement for the model: It needs to have a column `image_meta_data`. This is a serialised hash where paperdragon saves UIDs for the different image versions. We'll learn about this in a minute.
33 |
34 | ```ruby
35 | class User < ActiveRecord::Base # this could be just anything.
36 | include Paperdragon::Model
37 |
38 | processable :image
39 |
40 | serialize :image_meta_data
41 | end
42 | ```
43 |
44 | Calling `::processable` advises paperdragon to create a `User#image` reader to the attachment. Nothing else is added to the class.
45 |
46 |
47 | ## Uploading
48 |
49 | Processing and storing an uploaded image is an explicit step - you have to code it! This code usually goes to a separate class or an [Operation in Trailblazer](https://github.com/trailblazer/trailblazer#operation), don't leave it in the controller if you don't have to.
50 |
51 | ```ruby
52 | def create
53 | file = params.delete(:image)
54 |
55 | user = User.create(params) # this is your code.
56 |
57 | # upload code:
58 | user.image(file) do |v|
59 | v.process!(:original) # save the unprocessed.
60 | v.process!(:thumb) { |job| job.thumb!("75x75#") } # resizing.
61 | v.process!(:cropped) { |job| job.thumb!("140x140+20+20") } # cropping.
62 | v.process!(:public) { |job| job.watermark! } # watermark.
63 | end
64 |
65 | user.save
66 | end
67 | ```
68 |
69 | This is a completely transparent process.
70 |
71 | 1. Calling `#image` usually returns the image attachment. However, passing a `file` to it allows to create different versions of the uploaded image in the block.
72 | 2. `#process!` requires you to pass in a name for that particular image version. It is a convention to call the unprocessed image `:original`.
73 | 3. The `job` object is responsible for creating the final version. This is simply a `Dragonfly::Job` object and gives you [everything that can be done with dragonfly](http://markevans.github.io/dragonfly/imagemagick/).
74 | 4. After the block is run, paperdragon pushes a hash with all the images meta data to the model via `model.image_meta_data=`.
75 |
76 | For a better understanding and to see how simple it is, go and check out the `image_meta_data` field.
77 |
78 | ```ruby
79 | user.image_meta_data #=> {original: {uid: "original-logo.jpg", width: 240, height: 800},
80 | # thumb: {uid: "thumb-logo.jpg", width: 140, height: 140},
81 | # ..and so on..
82 | # }
83 | ```
84 |
85 |
86 | ## Rendering Images
87 |
88 | After processing, you may want to render those image versions in your app.
89 |
90 | ```ruby
91 | user.image[:thumb].url
92 | ```
93 |
94 | This is all you need to retrieve the URL/path for a stored image. Use this for your image tags.
95 |
96 | ```haml
97 | = img_tag user.image[:thumb].url
98 | ```
99 |
100 | Internally, Paperdragon will call `model#image_meta_data` and use this hash to find the address of the image.
101 |
102 | While gems like paperclip often use several fields of the model to compute UIDs (addresses) at run-time, paperdragon does that once and then dumps it to the database. This completely removes the dependency to the model.
103 |
104 |
105 | ## Reprocessing And Cropping
106 |
107 | Once an image has been processed to several versions, you might need to reprocess some of them. As an example, users could re-crop their thumbs.
108 |
109 | ```ruby
110 | def crop
111 | user = User.find(params[:id]) # this is your code.
112 |
113 | # reprocessing code:
114 | cropping = "#{params[:w]}x#{params[:h]}#"
115 |
116 | user.image do |v|
117 | v.reprocess!(:thumb, Time.now) { |job| job.thumb!(cropping) } # re-crop.
118 | end
119 |
120 | user.save
121 | end
122 | ```
123 |
124 | Only a few things have changed compared to the initial processing.
125 |
126 | 1. We do not pass a file to `#image` anymore. This makes sense as reprocessing will re-use the existing original file.
127 | 2. Note that it's not `#process!` but `#reprocess!` indicating a surprising reprocessing.
128 | 3. As a second argument to `#reprocess!` a fingerprint string is required. To understand what this does, let's inspect `image_meta_data` once again. (The fingerprint feature is optional but extremely helpful.)
129 |
130 |
131 | ```ruby
132 | user.image_meta_data # ..original..
133 | # thumb: {uid: "thumb-logo-1234567890.jpg", width: 48, height: 48},
134 | # ..and so on..
135 | # }
136 | ```
137 |
138 | See how the file name has changed? Paperdragon will automatically append the fingerprint you pass into `#reprocess!` to the existing version's file name.
139 |
140 |
141 | ## Renaming
142 |
143 | Sometimes you just want to rename files without processing them. For instance, when a new fingerprint for an image is introduced, you want to apply that to all versions.
144 |
145 | ```ruby
146 | fingerprint = Time.now
147 |
148 | user.image do |v|
149 | v.reprocess!(:thumb, fingerprint) { |job| job.thumb!(cropping) } # re-crop.
150 | v.rename!(:original, fingerprint) # just rename it.
151 | end
152 | ```
153 |
154 | This will re-crop the thumb and _rename_ the original.
155 |
156 | ```ruby
157 | user.image_meta_data #=> {original: {uid: "original-logo-1234567890.jpg", ..},
158 | # thumb: {uid: "thumb-logo-1234567890.jpg", ..},
159 | # ..and so on..
160 | # }
161 | ```
162 |
163 |
164 | ## Deleting
165 |
166 | While making images is a wonderful thing, sometimes you need to destroy to create. This is why paperdragon gives you a deleting mechanism, too.
167 |
168 | ```ruby
169 | user.image do |v|
170 | v.delete!(:thumb)
171 | end
172 | ```
173 |
174 | This will also remove the associated metadata from the model.
175 |
176 | You can delete all versions of an attachment by omitting the style.
177 |
178 | ```ruby
179 | user.image do |v|
180 | v.delete! # deletes :original and :thumb.
181 | end
182 | ```
183 |
184 |
185 | ## Replacing Images
186 |
187 | It's ok to run `#process!` again on a model with an existing attachment.
188 |
189 | ```ruby
190 | user.image_meta_data #=> {original: {uid: "original-logo-1234567890.jpg", ..},
191 | ```
192 |
193 | Processing here will overwrite the existing attachment.
194 |
195 | ```ruby
196 | user.image(new_file) do |v|
197 | v.process!(:original) # overwrites the existing, deletes old.
198 | end
199 | ```
200 |
201 | ```ruby
202 | user.image_meta_data #=> {original: {uid: "original-new-file01.jpg", ..},
203 | ```
204 |
205 | While replacing the old with the new upload, the old file also gets deleted.
206 |
207 |
208 | ## Fingerprints
209 |
210 | Paperdragon comes with a very simple built-in file naming.
211 |
212 | Computing a file UID (or, name, or path) happens in the `Attachment` class. You need to provide your own implementation if you want to change things.
213 |
214 | ```ruby
215 | class User < ActiveRecord::Base
216 | include Paperdragon::Model
217 |
218 | class Attachment < Paperdragon::Attachment
219 | def build_uid(style, file)
220 | "/path/to/#{style}/#{obfuscator}/#{file.name}"
221 | end
222 |
223 | def obfuscator
224 | Obfuscator.call # this is your code.
225 | end
226 | end
227 |
228 | processable :image, Attachment # use the class you just wrote.
229 | ```
230 |
231 | The `Attachment#build_uid` method is invoked when processing images.
232 |
233 | ```ruby
234 | user.image(file) do |v|
235 | v.process!(:thumb) { |job| job.thumb!("75x75#") }
236 | end
237 | ```
238 |
239 | To create the image UID, _your_ attachment is now being used.
240 |
241 | ```ruby
242 | user.image_meta_data # ..original..
243 | # thumb: {uid: "/path/to/thumb/ac97dnxid8/logo.jpg", ..},
244 | # ..and so on..
245 | # }
246 | ```
247 |
248 | What a beautiful, cryptic and mysterious filename you just created!
249 |
250 | The same pattern applies for _re-building_ UIDs when reprocessing images.
251 |
252 | ```ruby
253 | class Attachment < Paperdragon::Attachment
254 | # def build_uid and the other code from above..
255 |
256 | def rebuild_uid(file, fingerprint)
257 | file.uid.sub("logo.png", "logo-#{fingerprint}.png")
258 | end
259 | end
260 | ```
261 |
262 | This code is used to re-compute UIDs in `#reprocess!`.
263 |
264 | That example is stupid, I know, but it shows how you have access to the `Paperdragon::File` instance that represents the existing version of the reprocessed image.
265 |
266 |
267 | ## Local Rails Configuration
268 |
269 | Configuration of paperdragon completely relies on [configuring dragonfly](http://markevans.github.io/dragonfly/configuration/). As an example, for a Rails app with a local file storage, I use the following configuration in `config/initializers/paperdragon.rb`.
270 |
271 | ```ruby
272 | Dragonfly.app.configure do
273 | plugin :imagemagick
274 |
275 | datastore :file,
276 | :server_root => 'public',
277 | :root_path => 'public/images'
278 | end
279 | ```
280 |
281 | This would result in image UIDs being prefixed accordingly.
282 |
283 | ```ruby
284 | user.image[:thumb].url #=> "/images/logo-1234567890.png"
285 | ```
286 |
287 |
288 | ## S3
289 |
290 | As [dragonfly allows S3](https://github.com/markevans/dragonfly-s3_data_store), using the amazon cloud service is straight-forward.
291 |
292 | All you need to do is configuring your bucket. The API for paperdragon remains unchanged.
293 |
294 | ```ruby
295 | require 'dragonfly/s3_data_store'
296 |
297 | Dragonfly.app.configure do
298 | datastore :s3,
299 | bucket_name: 'my-bucket',
300 | access_key_id: 'blahblahblah',
301 | secret_access_key: 'blublublublu'
302 | end
303 | ```
304 |
305 | Images will be stored "in the cloud" when using `#process!`, renaming, deleting and re-processing do the same!
306 |
307 |
308 | ## Background Processing
309 |
310 | The explicit design of paperdragon makes it incredibly simple to move all or certain processing steps to background jobs.
311 |
312 | ```ruby
313 | class Image::Processor
314 | include Sidekiq::Worker
315 |
316 | def perform(params)
317 | user = User.find(params[:id])
318 |
319 | user.image(params[:file]) do |v|
320 | v.process!(:original)
321 | end
322 | end
323 | end
324 | ```
325 |
326 | Documentation how to use Sidekiq and paperdragon in Traiblazer will be added shortly.
327 |
328 | ## Validations
329 |
330 | Validating uploads are discussed in the _Callbacks_ chapter of the [Trailblazer
331 | book](https://leanpub.com/trailblazer). We use [file_validators](https://github.com/musaffa/file_validators).
332 |
333 | ## Model: Reader and Writer
334 |
335 | If you don't like `Paperdragon::Model#image`'s fuzzy API you can use `Reader` and `Writer`.
336 |
337 | The `Writer` will usually be mixed into a form.
338 |
339 | ```ruby
340 | class AlbumForm < Reform::Form
341 | extend Paperdragon::Model::Writer
342 | processable_writer :image
343 | ```
344 |
345 | This provides the `image!` writer for processing a file.
346 |
347 | ```ruby
348 | form.image!(file) { |v| v.thumb!("64x64") }
349 | ```
350 |
351 | Likewise, `Reader` will usually be used in cells or decorators.
352 |
353 | ```ruby
354 | class AlbumCell < Cell::ViewModel
355 | extend Paperdragon::Model::Reader
356 | processable_reader :image
357 | property :image_meta_data
358 | ```
359 |
360 | You can now access the `Attachment` via `image`.
361 |
362 | ```ruby
363 | cell.image[:thumb].url
364 | ```
365 |
366 |
367 | ## Paperclip compatibility
368 |
369 | I wrote paperdragon as an explicit alternative to paperclip. In the process of doing so, I step-wise replaced upload code, but left the rendering code unchanged. Paperclip has a slightly different API for rendering.
370 |
371 | ```ruby
372 | user.image.url(:thumb)
373 | ```
374 |
375 | Allowing your paperdragon-backed model to expose this API is piece-of-cake.
376 |
377 | ```ruby
378 | class User < ActiveRecord::Base
379 | include Paperdragon::Paperclip::Model
380 | ```
381 |
382 | This will allow both APIs for a smooth transition.
383 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rake/testtask"
3 |
4 | task :default => [:test]
5 | Rake::TestTask.new(:test) do |test|
6 | test.libs << 'test'
7 | test.test_files = FileList['test/**/*_test.rb']
8 | test.verbose = true
9 | end
--------------------------------------------------------------------------------
/lib/paperdragon.rb:
--------------------------------------------------------------------------------
1 | require "paperdragon/version"
2 | require 'dragonfly'
3 |
4 | module Paperdragon
5 | class MissingUploadError < RuntimeError
6 | end
7 | end
8 |
9 | require 'paperdragon/file'
10 | require 'paperdragon/metadata'
11 | require 'paperdragon/attachment'
12 | require 'paperdragon/task'
13 | require 'paperdragon/model'
--------------------------------------------------------------------------------
/lib/paperdragon/attachment.rb:
--------------------------------------------------------------------------------
1 | require 'uber/inheritable_attr'
2 |
3 | module Paperdragon
4 | # Override #build_uid / #rebuild_uid.
5 | # Note that we only encode the UID when computing it the first time. It is then stored encoded
6 | # and no escaping happens at any time after that.
7 | # You may use options.
8 | # only saves metadata, does not know about model.
9 | # Attachment is a builder for File and knows about metadata. It is responsible for creating UID if metadata is empty.
10 | class Attachment
11 | extend Uber::InheritableAttr
12 | inheritable_attr :file_class #, strategy: ->{ tap{} }
13 | self.file_class = ::Paperdragon::File # default value. # !!! be careful, this gets cloned in subclasses and thereby becomes a subclass of PD:File.
14 |
15 |
16 | module InstanceMethods
17 | def initialize(metadata, options={})
18 | @metadata = Metadata[metadata]
19 | @options = options # to be used in #(re)build_uid for your convenience. # DISCUSS: we pass in the model here - is that what we want?
20 | end
21 | attr_reader :metadata # TODO: test me.
22 |
23 | def [](style, file=nil) # not sure if i like passing file here, consider this method signature semi-public.
24 | file_metadata = @metadata[style]
25 |
26 | uid = file_metadata[:uid] || uid_from(style, file)
27 | self.class.file_class.new(uid, file_metadata)
28 | end
29 |
30 | # DSL method providing the task instance.
31 | # When called with block, it yields the task and returns the generated metadata.
32 | def task(upload=nil, &block)
33 | task = Task.new(self, upload, &block)
34 |
35 | return task unless block_given?
36 | task.metadata_hash
37 | end
38 |
39 | # Computes UID when File doesn't have one, yet. Called in #initialize.
40 | def uid_from(*args)
41 | build_uid(*args)
42 | end
43 |
44 | # Per default, paperdragon tries to increment the fingerprint in the file name, identified by
45 | # the pattern /-\d{10}/ just before the filename extension (.png).
46 | def rebuild_uid(file, fingerprint=nil) # the signature of this method is to be considered semi-private.
47 | ext = ::File.extname(file.uid)
48 | name = ::File.basename(file.uid, ext)
49 |
50 | if fingerprint and matches = name.match(/-(\d{10})$/)
51 | return file.uid.sub(matches[1], fingerprint.to_s)
52 | end
53 |
54 | file.uid.sub(name, "#{name}-#{fingerprint}")
55 | end
56 |
57 | def exists? # should be #uploaded? or #stored?
58 | # not sure if i like that kind of state here, so consider method semi-public.
59 | @metadata.populated?
60 | end
61 |
62 | private
63 | attr_reader :options
64 |
65 | def build_uid(style, file)
66 | # can we use Dragonfly's API here?
67 | "#{style}-#{Dragonfly::TempObject.new(file).name}"
68 | end
69 | end
70 |
71 |
72 | module SanitizeUid
73 | def uid_from(*args)
74 | sanitize(super)
75 | end
76 |
77 | def sanitize(uid)
78 | #URI::encode(uid) # this is wrong, we can't send %21 in path to S3!
79 | uid.gsub(/(#|\?)/, "_") # escape # and ?, only.
80 | end
81 | end
82 |
83 |
84 | include InstanceMethods
85 | include SanitizeUid # overrides #uid_from.
86 | end
87 | end
--------------------------------------------------------------------------------
/lib/paperdragon/file.rb:
--------------------------------------------------------------------------------
1 | module Paperdragon
2 | # A physical file with a UID.
3 | #
4 | # Files are usually created via an Attachment instance. You can call processing
5 | # methods on file instances. This will save the file and return the new metadata
6 | # hash.
7 | #
8 | # file = Paperdragon::File.new(uid)
9 | #
10 | # metadata = file.reprocess! do |job|
11 | # job.thumb!("16x16")
12 | # end
13 | class File
14 | def initialize(uid, options={})
15 | @uid = uid
16 | @options = options
17 | @data = nil # DISCUSS: do we need that here?
18 | end
19 |
20 | attr_reader :uid, :options
21 | alias_method :metadata, :options
22 |
23 | def url(opts={})
24 | Dragonfly.app.remote_url_for(uid, opts)
25 | end
26 |
27 | def data
28 | puts "........................FETCH (data): #{uid}, #{@data ? :cached : (:fetching)}"
29 | @data ||= Dragonfly.app.fetch(uid).data
30 | end
31 |
32 | # attr_reader :meta_data
33 |
34 | require 'paperdragon/file/operations'
35 | include Process
36 | include Delete
37 | include Reprocess
38 | include Rename
39 |
40 |
41 | private
42 | # replaces the UID.
43 | def uid!(new_uid)
44 | @uid = new_uid
45 | end
46 |
47 | # Override if you want to include/exclude properties in this file metadata.
48 | def default_metadata_for(job)
49 | {:width => job.width, :height => job.height, :uid => uid}#, :content_type => job.mime_type}
50 | end
51 |
52 | def metadata_for(job, additional={})
53 | default_metadata_for(job).merge(additional)
54 | end
55 | end
56 | end
--------------------------------------------------------------------------------
/lib/paperdragon/file/operations.rb:
--------------------------------------------------------------------------------
1 | module Paperdragon
2 | class File
3 | # DISCUSS: allow the metadata passing here or not?
4 | module Process
5 | def process!(file, new_uid=nil, metadata={})
6 | job = Dragonfly.app.new_job(file)
7 |
8 | yield job if block_given?
9 |
10 | old_uid = uid
11 | uid!(new_uid) if new_uid # set new uid if this is a replace.
12 |
13 | upload!(job, old_uid, new_uid, metadata)
14 | end
15 |
16 | private
17 | # Upload file, delete old file if there is one.
18 | def upload!(job, old_uid, new_uid, metadata)
19 | puts "........................STORE (process): #{uid}"
20 | job.store(path: uid, :headers => {'x-amz-acl' => 'public-read', "Content-Type" => "image/jpeg"})
21 |
22 | if new_uid # new uid means delete old one.
23 | puts "........................DELETE (reprocess): #{old_uid}"
24 | Dragonfly.app.destroy(old_uid)
25 | end
26 |
27 | @data = nil
28 | metadata_for(job, metadata)
29 | end
30 | end
31 |
32 |
33 | module Delete
34 | def delete!
35 | puts "........................DELETE (delete): #{uid}"
36 | Dragonfly.app.destroy(uid)
37 | end
38 | end
39 |
40 |
41 | module Reprocess
42 | def reprocess!(new_uid, original, metadata={})
43 | job = Dragonfly.app.new_job(original.data) # inheritance here somehow?
44 |
45 | yield job if block_given?
46 |
47 | old_uid = uid
48 | uid!(new_uid) # new UID is already computed and set.
49 |
50 | upload!(job, old_uid, new_uid, metadata)
51 | end
52 | end
53 |
54 |
55 | module Rename
56 | def rename!(fingerprint, metadata={}) # fixme: we are currently ignoring the custom metadata.
57 | old_uid = uid
58 | uid!(fingerprint)
59 |
60 | puts "........................MV:
61 | #{old_uid}
62 | #{uid}"
63 | # dragonfly_s3 = Dragonfly.app.datastore
64 | # Dragonfly.app.datastore.storage.copy_object(dragonfly_s3.bucket_name, old_uid, dragonfly_s3.bucket_name, uid, {'x-amz-acl' => 'public-read', "Content-Type" => "image/jpeg"})
65 | yield old_uid, uid
66 |
67 | puts "........................DELETE: #{old_uid}"
68 | Dragonfly.app.destroy(old_uid)
69 |
70 |
71 | self.metadata.merge(:uid => uid) # usually, metadata is already set to the old metadata when File was created via Attachment.
72 | end
73 | end
74 | end
75 | end
--------------------------------------------------------------------------------
/lib/paperdragon/metadata.rb:
--------------------------------------------------------------------------------
1 | module Paperdragon
2 | # 2-level meta data hash for a file. Returns empty string if not found.
3 | # Metadata.new(nil)[:original][:width] => ""
4 | # Holds metadata for an attachment. This is a hash keyed by versions, e.g. +:original+,
5 | # +:thumb+, and so on.
6 | class Metadata < Hash
7 | def self.[](hash) # allow Metadata[nil]
8 | super hash || {}
9 | end
10 |
11 | def [](name)
12 | super || {}
13 | end
14 |
15 | def populated?
16 | size > 0
17 | end
18 |
19 | # Consider this semi-public. This is used the make the metadata hash serialisable (as a plain hash).
20 | def to_hash
21 | Hash[self]
22 | end
23 | end
24 | end
--------------------------------------------------------------------------------
/lib/paperdragon/model.rb:
--------------------------------------------------------------------------------
1 | module Paperdragon
2 | # Fuzzy API: gives you #image that can do both upload and rendering.
3 | module Model
4 | def self.included(base)
5 | base.extend ClassMethods
6 | end
7 |
8 | module ClassMethods
9 | def processable(name, attachment_class=Attachment)
10 | include attachment_accessor_for(name, attachment_class)
11 | end
12 |
13 | private
14 | # Creates Avatar#image that returns a Paperdragon::File instance.
15 | def attachment_accessor_for(name, attachment_class)
16 | mod = Module.new do # TODO: abstract that into Uber, we use it everywhere.
17 | define_method name do |file=nil, options={}, &block|
18 | attachment = attachment_class.new(public_send("#{name}_meta_data"),
19 | options.merge(model: self))
20 |
21 | return attachment unless file or block
22 |
23 | # run the task block and save the returned new metadata in the model.
24 | self.public_send("#{name}_meta_data=", attachment.task(*[file], &block))
25 | end
26 | end
27 | end
28 | end
29 |
30 |
31 | # class Album
32 | # extend Paperdragon::Model::Writer
33 | # processable_writer :image
34 | #
35 | # Provides Album#image!(file) { |v| v.thumb!("64x64") }
36 | module Writer
37 | def processable_writer(name, attachment_class=Attachment)
38 | mod = Module.new do # TODO: abstract that into Uber, we use it everywhere.
39 | define_method "#{name}!" do |file=nil, options={}, &block|
40 | attachment = attachment_class.new(public_send("#{name}_meta_data"),
41 | options.merge(model: self))
42 |
43 | # run the task block and save the returned new metadata in the model.
44 | self.public_send("#{name}_meta_data=", attachment.task(*[file], &block))
45 | end
46 | end
47 | include mod
48 | end
49 | end # Writer.
50 |
51 |
52 | # class Album
53 | # extend Paperdragon::Model::Reader
54 | # processable_reader :image
55 | #
56 | # Provides Album#image #=> Attachment.
57 | module Reader
58 | def processable_reader(name, attachment_class=Attachment)
59 | define_method name do
60 | attachment_class.new(public_send("#{name}_meta_data"), model: self)
61 | end
62 | end
63 | end
64 | end
65 | end
--------------------------------------------------------------------------------
/lib/paperdragon/paperclip.rb:
--------------------------------------------------------------------------------
1 | module Paperdragon
2 | class Paperclip
3 |
4 | # Compute a UID to be compatible with paperclip. This class is meant to be subclassed so you can write
5 | # your specific file path.
6 | # Immutable
7 | class Uid
8 | def self.from(options)
9 | new(options).call
10 | end
11 |
12 | # "/system/:class/:attachment/:id_partition/:style/:filename"
13 | def initialize(options)
14 | @class_name = options[:class_name]
15 | @attachment = options[:attachment]
16 | @id = options[:id]
17 | @style = options[:style]
18 | @updated_at = options[:updated_at]
19 | @file_name = options[:file_name]
20 | @hash_secret = options[:hash_secret]
21 | @fingerprint = options[:fingerprint] # not used in default.
22 | end
23 |
24 | def call
25 | # default:
26 | # system/:class/:attachment/:id_partition/:style/:filename
27 | "#{root}/#{class_name}/#{attachment}/#{id_partition}/#{hash}/#{style}/#{file_name}"
28 | end
29 |
30 | private
31 | attr_reader :class_name, :attachment, :id, :style, :file_name, :hash_secret, :updated_at, :fingerprint
32 |
33 | def root
34 | "system"
35 | end
36 |
37 | def id_partition
38 | IdPartition.call(id)
39 | end
40 |
41 | def hash
42 | HashKey.call(hash_secret, class_name, attachment, id, style, updated_at)
43 | end
44 |
45 |
46 | class IdPartition
47 | def self.call(id)
48 | ("%09d" % id).scan(/\d{3}/).join("/") # FIXME: only works with integers.
49 | end
50 | end
51 |
52 |
53 | # ":class/:attachment/:id/:style/:updated_at"
54 | class HashKey
55 | require 'openssl' unless defined?(OpenSSL)
56 |
57 | # cover_girls/images/4841/thumb/1402617353
58 | def self.call(secret, class_name, attachment, id, style, updated_at)
59 | data = "#{class_name}/#{attachment}/#{id}/#{style}/#{updated_at}"
60 | # puts "[Paperdragon] HashKey <--------------------- #{data}"
61 | OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, data)
62 | end
63 | end
64 | end
65 | end
66 | end
67 |
68 | require 'paperdragon/paperclip/model'
--------------------------------------------------------------------------------
/lib/paperdragon/paperclip/model.rb:
--------------------------------------------------------------------------------
1 | module Paperdragon
2 | class Paperclip
3 | # DISCUSS: I want to remove this module.
4 | module Model
5 | def self.included(base)
6 | base.extend ClassMethods
7 | end
8 |
9 | module ClassMethods
10 | def processable(name, attachment_class)
11 | # this overrides #image (or whatever the name is) from Paperclip::Model::processable.
12 | # This allows using both paperclip's `image.url(:thumb)` and the new paperdragon style
13 | # `image(:thumb).url`.
14 | mod = Module.new do # TODO: merge with attachment_accessor_for.
15 | define_method name do # e.g. Avatar#image
16 | Proxy.new(name, self, attachment_class) # provide paperclip DSL.
17 | end
18 | end
19 | include mod
20 | end
21 | end
22 |
23 |
24 | # Needed to expose Paperclip's DSL, like avatar.image.url(:thumb).
25 | class Proxy
26 | def initialize(name, model, attachment_class)
27 | @attachment = attachment_class.new(model.public_send("#{name}_meta_data"), {:model => model})
28 | end
29 |
30 | def url(style)
31 | @attachment[style].url # Avatar::Photo.new(avatar, :thumb).url
32 | end
33 |
34 | def method_missing(name, *args, &block)
35 | @attachment.send(name, *args, &block)
36 | end
37 | end
38 | end
39 | end
40 | end
--------------------------------------------------------------------------------
/lib/paperdragon/task.rb:
--------------------------------------------------------------------------------
1 | module Paperdragon
2 | # Gives a simple API for processing multiple versions of a single attachment.
3 | #
4 | # Each processing method will return the updated metadata hash. You still have
5 | # to save that hash back to the model.
6 | class Task
7 | def initialize(attachment, upload=nil)
8 | @attachment = attachment
9 | @upload = upload
10 | @metadata = attachment.metadata.dup # DISCUSS: keep this dependency?
11 |
12 | yield self if block_given?
13 | end
14 |
15 | attr_reader :metadata
16 | def metadata_hash # semi-private, might be removed.
17 | metadata.to_hash
18 | end
19 |
20 | # process!(style, [*args,] &block) :
21 | # version = CoverGirl::Photo.new(@model, style, *args)
22 | # metadata = version.process!(upload, &block)
23 | # merge! {style => metadata}
24 | def process!(style, &block)
25 | version = file(style, upload)
26 | new_uid = new_uid_for(style, version) # new uid when overwriting existing attachment.
27 |
28 | @metadata.merge!(style => version.process!(upload, new_uid, &block))
29 | end
30 |
31 | # fingerprint optional => filename is gonna remain the same
32 | # original nil => use [:original]
33 | def reprocess!(style, fingerprint=nil, original=nil, &block)
34 | @original ||= file(:original) # this is cached per task instance.
35 | version = file(style)
36 | new_uid = @attachment.rebuild_uid(version, fingerprint)
37 |
38 | @metadata.merge!(style => version.reprocess!(new_uid, @original, &block))
39 | end
40 |
41 | def rename!(style, fingerprint, &block)
42 | version = file(style)
43 | new_uid = @attachment.rebuild_uid(version, fingerprint)
44 |
45 | @metadata.merge!(style => version.rename!(new_uid, &block))
46 | end
47 |
48 | def delete!(*styles)
49 | styles = @attachment.metadata.keys if styles.size == 0
50 |
51 | styles.each do |style|
52 | file(style).delete!
53 | @metadata.delete(style)
54 | end
55 |
56 | @metadata
57 | end
58 |
59 | private
60 | def file(style, upload=nil)
61 | @attachment[style, upload]
62 | end
63 |
64 | def upload
65 | @upload or raise MissingUploadError.new("You called #process! but didn't pass an uploaded file to Attachment#task.")
66 | end
67 |
68 | # Returns new UID for new file when overriding an existing attachment with #process!.
69 | def new_uid_for(style, version)
70 | # check if UID is present in existing metadata.
71 | @attachment.metadata[style][:uid] ? @attachment.uid_from(style, upload) : nil # DISCUSS: move to Attachment?
72 | end
73 | end
74 | end
--------------------------------------------------------------------------------
/lib/paperdragon/version.rb:
--------------------------------------------------------------------------------
1 | module Paperdragon
2 | VERSION = "0.0.11"
3 | end
4 |
--------------------------------------------------------------------------------
/paperdragon.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'paperdragon/version'
5 |
6 | Gem::Specification.new do |spec|
7 | spec.name = "paperdragon"
8 | spec.version = Paperdragon::VERSION
9 | spec.authors = ["Nick Sutterer"]
10 | spec.email = ["apotonick@gmail.com"]
11 | spec.summary = %q{Explicit image processing based on Dragonfly with Paperclip compatibility.}
12 | spec.description = %q{Explicit image processing based on Dragonfly with Paperclip compatibility.}
13 | spec.homepage = ""
14 | spec.license = "MIT"
15 |
16 | spec.files = `git ls-files -z`.split("\x0")
17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19 | spec.require_paths = ["lib"]
20 |
21 | spec.add_dependency "dragonfly", "~> 1.0.12"
22 | spec.add_dependency "uber"
23 |
24 | spec.add_development_dependency "bundler", "~> 1.6"
25 | spec.add_development_dependency "rake"
26 | spec.add_development_dependency "minitest"
27 | end
28 |
--------------------------------------------------------------------------------
/test/attachment_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class AttachmentSpec < MiniTest::Spec
4 | class Attachment < Paperdragon::Attachment
5 | private
6 | def uid_from(style, file)
7 | "/uid/#{style}"
8 | end
9 | end
10 |
11 | describe "existing" do
12 | subject { Attachment.new({:original => {:uid=>"/uid/1234.jpg", :width => 99}}) }
13 |
14 | it { subject[:original].uid.must_equal "/uid/1234.jpg" }
15 | it { subject[:original].options.must_equal({:uid=>"/uid/1234.jpg", :width => 99}) }
16 | it { subject.exists?.must_equal true }
17 | end
18 |
19 | describe "new" do
20 | subject { Attachment.new(nil) }
21 |
22 | it { subject[:original].uid.must_equal "/uid/original" }
23 | it { subject[:original].options.must_equal({}) }
24 | it { subject.exists?.must_equal false }
25 | end
26 |
27 | describe "new with empty metadata hash" do
28 | subject { Attachment.new({}) }
29 |
30 | it { subject[:original].uid.must_equal "/uid/original" }
31 | it { subject[:original].options.must_equal({}) }
32 | it { subject.exists?.must_equal false }
33 | end
34 |
35 | # #metadata
36 | it { Attachment.new({}).metadata.to_hash.must_equal( {}) }
37 |
38 |
39 | # test passing options into Attachment and use that in #build_uid.
40 | class AttachmentUsingOptions < Paperdragon::Attachment
41 | private
42 | def build_uid(style, file)
43 | "uid/#{style}/#{options[:filename]}"
44 | end
45 | end
46 |
47 | # use in new --> build_uid.
48 | it { AttachmentUsingOptions.new(nil, {:filename => "apotomo.png"})[:original].uid.must_equal "uid/original/apotomo.png" }
49 |
50 |
51 | # test using custom File class in Attachment.
52 | class OverridingAttachment < Paperdragon::Attachment
53 | class File < Paperdragon::File
54 | def uid
55 | "from/file"
56 | end
57 | end
58 | self.file_class= File
59 | end
60 |
61 | it { OverridingAttachment.new(nil)[:original, Pathname.new("not-considered.JPEG")].uid.must_equal "from/file" }
62 |
63 |
64 | # test UID sanitising. this happens only when computing the UID with a new attachment!
65 | describe "insane filename" do
66 | it { AttachmentUsingOptions.new(nil, {:filename => "(use)? apotomo #1#.png"})[:original].uid.must_equal "uid/original/(use)_ apotomo _1_.png" }
67 | end
68 | end
69 |
70 |
71 | class AttachmentModelSpec < MiniTest::Spec
72 | class Attachment < Paperdragon::Attachment
73 | private
74 | def build_uid(style, file)
75 | "#{options[:model].class}/uid/#{style}/#{options[:filename]}"
76 | end
77 | end
78 |
79 | describe "existing" do
80 | let (:existing) { OpenStruct.new(:image_meta_data => {:original => {:uid=>"/uid/1234.jpg"}}) }
81 | subject { Attachment.new(existing.image_meta_data, :model => existing) }
82 |
83 | it { subject[:original].uid.must_equal "/uid/1234.jpg" } # notice that #uid_from is not called.
84 | end
85 |
86 | describe "new" do
87 | subject { Attachment.new(nil, :filename => "apotomo.png", :model => OpenStruct.new) } # you can pass options into Attachment::new that may be used in #build_uid
88 |
89 | it { subject[:original].uid.must_equal "OpenStruct/uid/original/apotomo.png" }
90 | end
91 | end
--------------------------------------------------------------------------------
/test/file_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | Dragonfly.app.configure do
4 | plugin :imagemagick
5 |
6 | #url_host 'http://some.domain.com:4000'
7 |
8 | datastore :file,
9 | :server_root => 'public',
10 | :root_path => 'public/paperdragon'
11 | end
12 |
13 | class PaperdragonFileTest < MiniTest::Spec
14 | it { Paperdragon::File.new("123").uid.must_equal "123" }
15 | it { Paperdragon::File.new("123").url.must_equal "/paperdragon/123" } # FIXME: how to add host?
16 | it { Paperdragon::File.new("123") }
17 |
18 | describe "#metadata" do
19 | it { Paperdragon::File.new("123").metadata.must_equal({}) }
20 | it { Paperdragon::File.new("123", :width => 16).metadata.must_equal({:width => 16}) }
21 | end
22 |
23 | describe "#data" do
24 | it do
25 | Paperdragon::File.new(uid).process!(logo)
26 | Paperdragon::File.new(uid).data.size.must_equal 9632
27 | end
28 | end
29 |
30 | let (:logo) { Pathname("test/fixtures/apotomo.png") }
31 |
32 | # process! saves file
33 | # TODO: remote storage, server root, etc.
34 | let (:uid) { generate_uid }
35 |
36 |
37 | describe "#process!" do
38 | let (:file) { file = Paperdragon::File.new(uid) }
39 |
40 | it do
41 | metadata = file.process!(logo)
42 |
43 | metadata.must_equal({:width=>216, :height=>63, :uid=>uid})
44 | exists?(uid).must_equal true
45 | end
46 |
47 | # block
48 | it do
49 | # puts file.data.size # 9632 bytes
50 | file.process!(logo) do |job|
51 | job.thumb!("16x16")
52 | end
53 |
54 | assert file.data.size < 500 # smaller after thumb!
55 | end
56 |
57 | # additional metadata
58 | it do
59 | file.process!(logo, nil, :cropping => "16x16") do |job|
60 | job.thumb!("16x16")
61 | end.must_equal({:width=>16, :height=>5, :uid=>uid, :cropping=>"16x16"})
62 | end
63 | end
64 |
65 |
66 | describe "#delete!" do
67 | it do
68 | file = Paperdragon::File.new(uid)
69 | file.process!(logo)
70 | exists?(uid).must_equal true
71 |
72 | job = Paperdragon::File.new(uid).delete!
73 |
74 | job.must_equal nil
75 | exists?(uid).must_equal false
76 | end
77 | end
78 |
79 |
80 | describe "#reprocess!" do
81 | # existing:
82 | let (:file) { Paperdragon::File.new(uid) }
83 | let (:original) { Paperdragon::File.new("original/#{uid}") }
84 | let (:new_uid) { generate_uid }
85 |
86 | before do
87 | original.process!(logo) # original/uid exists.
88 | exists?(original.uid).must_equal true
89 | file.process!(logo)
90 | exists?(file.uid).must_equal true # file to be reprocessed exists (to test delete).
91 | end
92 |
93 | it do
94 | metadata = file.reprocess!(new_uid, original)
95 |
96 | # it
97 | metadata.must_equal({:width=>216, :height=>63, :uid=>new_uid})
98 | # it
99 | exists?(uid).must_equal false # deleted
100 | exists?(new_uid).must_equal true
101 | end
102 |
103 | it do
104 | job = file.reprocess!(new_uid, original) do |j|
105 | j.thumb!("16x16")
106 |
107 | end
108 |
109 | assert file.data.size < 500
110 | end
111 | end
112 |
113 |
114 | describe "#rename!" do
115 | # existing:
116 | let (:file) { Paperdragon::File.new(uid, :size => 99) }
117 | let (:original) { Paperdragon::File.new(uid) }
118 | let (:new_uid) { generate_uid }
119 |
120 | before do
121 | original.process!(logo)
122 | exists?(uid).must_equal true
123 | end
124 |
125 | it do
126 | metadata = file.rename!(new_uid) do |uid, new_uid|
127 | File.rename("public/paperdragon/"+uid, "public/paperdragon/"+new_uid) # DISCUSS: should that be simpler?
128 | end
129 |
130 | # it
131 | # metadata.must_equal({:width=>216, :height=>63, :uid=>new_uid, :content_type=>"application/octet-stream", :size=>9632})
132 | metadata.must_equal(:uid=>new_uid, :size => 99) # we DON'T fetch original metadata here anymore.
133 |
134 | exists?(uid).must_equal false # deleted
135 | exists?(new_uid).must_equal true
136 | end
137 | end
138 | end
139 |
--------------------------------------------------------------------------------
/test/fixtures/apotomo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apotonick/paperdragon/d4075f8fad095bf387cafe67f55214218a28c52d/test/fixtures/apotomo.png
--------------------------------------------------------------------------------
/test/fixtures/trb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apotonick/paperdragon/d4075f8fad095bf387cafe67f55214218a28c52d/test/fixtures/trb.png
--------------------------------------------------------------------------------
/test/metadata_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class MetadataTest < MiniTest::Spec
4 | describe "valid" do
5 | let (:valid) {
6 | {
7 | :original=>{:width=>960, :height=>960, :uid=>"403661339/kristylee-38.jpg", :content_type=>"image/jpeg", :size=>198299},
8 | :thumb =>{:width=>191, :height=>191, :uid=>"ds3661339/kristylee-38.jpg", :content_type=>"image/jpeg", :size=>18132}
9 | }
10 | }
11 |
12 | subject { Paperdragon::Metadata[valid] }
13 |
14 | it { subject.populated?.must_equal true }
15 | it { subject[:original][:width].must_equal 960 }
16 | it { subject[:original][:uid].must_equal "403661339/kristylee-38.jpg" }
17 | it { subject[:thumb][:uid].must_equal "ds3661339/kristylee-38.jpg" }
18 |
19 | it { subject[:page].must_equal({}) }
20 | it { subject[:page][:width].must_equal nil }
21 | end
22 |
23 |
24 | describe "nil" do
25 | subject { Paperdragon::Metadata[nil] }
26 |
27 | it { subject.populated?.must_equal false }
28 | it { subject[:page].must_equal({}) }
29 | it { subject[:page][:width].must_equal nil }
30 | end
31 |
32 | describe "empty hash" do
33 | subject { Paperdragon::Metadata[{}] }
34 |
35 | it { subject.populated?.must_equal false }
36 | it { subject[:page].must_equal({}) }
37 | it { subject[:page][:width].must_equal nil }
38 | end
39 |
40 | let (:original) { {:original => {}} }
41 |
42 | # #dup
43 | # don't change original hash.
44 | it do
45 | Paperdragon::Metadata[original].dup.merge!(:additional => {})
46 | original[:additional].must_equal nil
47 | end
48 |
49 | # #to_hash
50 | it { Paperdragon::Metadata[original].to_hash.must_equal({:original=>{}}) }
51 | it { Paperdragon::Metadata[original].to_hash.class.must_equal(Hash) }
52 | end
--------------------------------------------------------------------------------
/test/model_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class PaperdragonModelTest < MiniTest::Spec
4 | class Avatar
5 | class Photo < Paperdragon::File
6 | end
7 |
8 | class Attachment < Paperdragon::Attachment
9 | self.file_class = Photo
10 | end
11 |
12 | include Paperdragon::Model
13 | processable :image, Attachment
14 | processable :picture, Attachment
15 |
16 |
17 | def image_meta_data
18 | {:thumb => {:uid => "Avatar-thumb"}}
19 | end
20 |
21 | def picture_meta_data
22 | {:thumb => {:uid => "Picture-thumb"}}
23 | end
24 | end
25 |
26 | # model has image_meta_data hash.
27 | it { Avatar.new.image[:thumb].url.must_equal "/paperdragon/Avatar-thumb" }
28 | it { Avatar.new.picture[:thumb].url.must_equal "/paperdragon/Picture-thumb" }
29 | # model doesn't have upload, yet. returns empty attachment.
30 | it { Image.new.image.metadata.must_equal({}) }
31 | it { Image.new.picture.metadata.must_equal({}) }
32 |
33 |
34 | # minimum setup
35 | class Image < OpenStruct
36 | include Paperdragon::Model
37 | processable :image
38 | processable :picture
39 | end
40 |
41 | it { Image.new(:image_meta_data => {:thumb => {:uid => "Avatar-thumb"}}).image[:thumb].url.must_equal "/paperdragon/Avatar-thumb" }
42 | it { Image.new(:picture_meta_data => {:thumb => {:uid => "Picture-thumb"}}).picture[:thumb].url.must_equal "/paperdragon/Picture-thumb" }
43 |
44 |
45 | # process with #image{}
46 | let (:logo) { Pathname("test/fixtures/apotomo.png") }
47 |
48 | it "image" do
49 | model = Image.new
50 | model.image(logo) do |v|
51 | v.process!(:original)
52 | v.process!(:thumb) { |j| j.thumb!("16x16") }
53 | end
54 |
55 | model.image_meta_data.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}, :thumb=>{:width=>16, :height=>5, :uid=>"thumb-apotomo.png"}})
56 |
57 |
58 | model.image do |v|
59 | v.reprocess!(:thumb, "1") { |j| j.thumb!("8x8") }
60 | end
61 |
62 | model.image_meta_data.class.must_equal Hash
63 | model.image_meta_data.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}, :thumb=>{:width=>8, :height=>2, :uid=>"thumb-apotomo-1.png"}})
64 |
65 | model.image do |v|
66 | v.delete!(:thumb)
67 | end
68 |
69 | model.image_meta_data.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}})
70 | end
71 |
72 | it "picture" do
73 | model = Image.new
74 | model.picture(logo) do |v|
75 | v.process!(:original)
76 | v.process!(:thumb) { |j| j.thumb!("16x16") }
77 | end
78 |
79 | model.picture_meta_data.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}, :thumb=>{:width=>16, :height=>5, :uid=>"thumb-apotomo.png"}})
80 |
81 |
82 | model.picture do |v|
83 | v.reprocess!(:thumb, "1") { |j| j.thumb!("8x8") }
84 | end
85 |
86 | model.picture_meta_data.class.must_equal Hash
87 | model.picture_meta_data.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}, :thumb=>{:width=>8, :height=>2, :uid=>"thumb-apotomo-1.png"}})
88 |
89 | model.picture do |v|
90 | v.delete!(:thumb)
91 | end
92 |
93 | model.picture_meta_data.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}})
94 | end
95 |
96 | # passing options from image(file, {..}) to the Attachment.
97 | class ImageWithAttachment < OpenStruct
98 | include Paperdragon::Model
99 |
100 | class Attachment < Paperdragon::Attachment
101 | def build_uid(style, file)
102 | @options.inspect # we use options here!
103 | end
104 |
105 | def rebuild_uid(style, file)
106 | @options.inspect
107 | end
108 | end
109 |
110 | processable :image, Attachment
111 | processable :picture, Attachment
112 | end
113 |
114 | it "image" do
115 | model = ImageWithAttachment.new
116 | model.image(logo, :path => "/") do |v|
117 | v.process!(:original)
118 | end
119 |
120 | # this assures that both :model and :path (see above) are passed through.
121 | model.image_meta_data.must_equal({:original=>{:width=>216, :height=>63, :uid=>"{:path=>\"/\", :model=>_}"}})
122 |
123 | model.image(nil, :new => true) do |v|
124 | v.reprocess!(:original, "1") { |j| j.thumb!("8x8") }
125 | end
126 |
127 | model.image_meta_data.must_equal({:original=>{:width=>8, :height=>2, :uid=>"{:new=>true, :model=>#{:width=>216, :height=>63, :uid=>\"{:path=>\\\"/\\\", :model=>_}\"}}>}"}})
128 | end
129 |
130 | it "picture" do
131 | model = ImageWithAttachment.new
132 | model.picture(logo, :path => "/") do |v|
133 | v.process!(:original)
134 | end
135 |
136 | # this assures that both :model and :path (see above) are passed through.
137 | model.picture_meta_data.must_equal({:original=>{:width=>216, :height=>63, :uid=>"{:path=>\"/\", :model=>_}"}})
138 |
139 | model.picture(nil, :new => true) do |v|
140 | v.reprocess!(:original, "1") { |j| j.thumb!("8x8") }
141 | end
142 |
143 | model.picture_meta_data.must_equal({:original=>{:width=>8, :height=>2, :uid=>"{:new=>true, :model=>#{:width=>216, :height=>63, :uid=>\"{:path=>\\\"/\\\", :model=>_}\"}}>}"}})
144 | end
145 | end
146 |
147 |
148 | class ModelWriterTest < MiniTest::Spec
149 | class Image < OpenStruct
150 | extend Paperdragon::Model::Writer
151 | processable_writer :image
152 | end
153 |
154 | # process with #image{}
155 | let (:logo) { Pathname("test/fixtures/apotomo.png") }
156 |
157 | it do
158 | model = Image.new
159 | model.image!(logo) do |v|
160 | v.process!(:original)
161 | v.process!(:thumb) { |j| j.thumb!("16x16") }
162 | end
163 |
164 | model.image_meta_data.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}, :thumb=>{:width=>16, :height=>5, :uid=>"thumb-apotomo.png"}})
165 |
166 |
167 | model.image! do |v|
168 | v.reprocess!(:thumb, "1") { |j| j.thumb!("8x8") }
169 | end
170 |
171 | model.image_meta_data.class.must_equal Hash
172 | model.image_meta_data.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}, :thumb=>{:width=>8, :height=>2, :uid=>"thumb-apotomo-1.png"}})
173 | end
174 |
175 | # Using another name
176 | class Picture < OpenStruct
177 | extend Paperdragon::Model::Writer
178 | processable_writer :picture
179 | end
180 |
181 | # process with #image{}
182 | let (:logo) { Pathname("test/fixtures/apotomo.png") }
183 |
184 | it do
185 | model = Picture.new
186 | model.picture!(logo) do |v|
187 | v.process!(:original)
188 | v.process!(:thumb) { |j| j.thumb!("16x16") }
189 | end
190 |
191 | model.picture_meta_data.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}, :thumb=>{:width=>16, :height=>5, :uid=>"thumb-apotomo.png"}})
192 |
193 |
194 | model.picture! do |v|
195 | v.reprocess!(:thumb, "1") { |j| j.thumb!("8x8") }
196 | end
197 |
198 | model.picture_meta_data.class.must_equal Hash
199 | model.picture_meta_data.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}, :thumb=>{:width=>8, :height=>2, :uid=>"thumb-apotomo-1.png"}})
200 | end
201 | end
202 |
203 | class ModelReaderTest < MiniTest::Spec
204 | class Image < OpenStruct
205 | extend Paperdragon::Model::Reader
206 | processable_reader :image
207 | end
208 |
209 | it { Image.new(:image_meta_data => {:thumb => {:uid => "Avatar-thumb"}}).image[:thumb].url.must_equal "/paperdragon/Avatar-thumb" }
210 |
211 | # Using another name
212 | class Image < OpenStruct
213 | extend Paperdragon::Model::Reader
214 | processable_reader :picture
215 | end
216 |
217 | it { Image.new(:picture_meta_data => {:thumb => {:uid => "Avatar-thumb"}}).picture[:thumb].url.must_equal "/paperdragon/Avatar-thumb" }
218 | end
--------------------------------------------------------------------------------
/test/paperclip_uid_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | require 'paperdragon/paperclip'
4 |
5 | class PaperclipUidTest < MiniTest::Spec
6 | Uid = Paperdragon::Paperclip::Uid
7 |
8 | let (:options) { {:class_name => :avatars, :attachment => :image, :id => 1234,
9 | :style => :original, :updated_at => Time.parse("20-06-2014 9:40:59 +1000").to_i,
10 | :file_name => "kristylee.jpg", :hash_secret => "secret"} }
11 |
12 | it { Uid.from(options).
13 | must_equal "system/avatars/image/000/001/234/9bf15e5874b3234c133f7500e6d615747f709e64/original/kristylee.jpg" }
14 |
15 |
16 | class UidWithFingerprint < Paperdragon::Paperclip::Uid
17 | def call
18 | "#{root}/#{class_name}/#{attachment}/#{id_partition}/#{hash}/#{style}/#{fingerprint}-#{file_name}"
19 | end
20 | end
21 |
22 | it { UidWithFingerprint.from(options.merge(:fingerprint => 8675309)).
23 | must_equal "system/avatars/image/000/001/234/9bf15e5874b3234c133f7500e6d615747f709e64/original/8675309-kristylee.jpg" }
24 | end
25 |
26 |
27 | class PaperclipModelTest < MiniTest::Spec
28 | class Avatar
29 | class Photo < Paperdragon::File
30 | end
31 |
32 | class Attachment < Paperdragon::Attachment
33 | self.file_class = Photo
34 |
35 | def exists?
36 | "Of course!"
37 | end
38 | end
39 |
40 | class PictureAttachment < Paperdragon::Attachment
41 | self.file_class = Photo
42 |
43 | def exists?
44 | "Of course it's a Picture!"
45 | end
46 | end
47 |
48 | include Paperdragon::Paperclip::Model
49 | processable :image, Attachment
50 | processable :picture, PictureAttachment
51 |
52 |
53 | def image_meta_data
54 | {:thumb => {:uid => "Avatar-thumb"}}
55 | end
56 |
57 | def picture_meta_data
58 | {:thumb => {:uid => "Picture-thumb"}}
59 | end
60 | end
61 |
62 | describe "image" do
63 | # old paperclip style
64 | it { Avatar.new.image.url(:thumb).must_equal "/paperdragon/Avatar-thumb" }
65 |
66 | # paperdragon style
67 | it { Avatar.new.image[:thumb].url.must_equal "/paperdragon/Avatar-thumb" }
68 |
69 | # delegates all unknown methods back to Attachment.
70 | it { Avatar.new.image.exists?.must_equal "Of course!" }
71 | end
72 |
73 | describe "picture" do
74 | # old paperclip style
75 | it { Avatar.new.picture.url(:thumb).must_equal "/paperdragon/Picture-thumb" }
76 |
77 | # paperdragon style
78 | it { Avatar.new.picture[:thumb].url.must_equal "/paperdragon/Picture-thumb" }
79 |
80 | # delegates all unknown methods back to Attachment.
81 | it { Avatar.new.picture.exists?.must_equal "Of course it's a Picture!" }
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/test/task_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class TaskSpec < MiniTest::Spec
4 | class Attachment < Paperdragon::Attachment
5 | class File < Paperdragon::File
6 | end
7 | self.file_class= File
8 | end
9 |
10 | let (:logo) { Pathname("test/fixtures/apotomo.png") }
11 |
12 |
13 | # #task allows block and returns metadata hash.
14 | describe "#task" do
15 | it do
16 | Attachment.new(nil).task(logo) do |t|
17 | t.process!(:original)
18 | t.process!(:thumb) { |j| j.thumb!("16x16") }
19 | end.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}, :thumb=>{:width=>16, :height=>5, :uid=>"thumb-apotomo.png"}})
20 | end
21 |
22 | # modify metadata in task
23 | it do
24 | Attachment.new({:processing => true, :approved => true}).task(logo) do |t|
25 | t.process!(:original)
26 | t.metadata.delete(:processing)
27 | end.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}, :approved => true})
28 | end
29 |
30 | end
31 |
32 | # task without block
33 | let (:subject) { Attachment.new(nil).task(logo) }
34 |
35 | describe "#process!" do
36 | it do
37 | subject.process!(:original)
38 | subject.process!(:thumb) { |j| j.thumb!("16x16") }
39 |
40 | original_metadata = subject.metadata_hash
41 | original_metadata.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}, :thumb=>{:width=>16, :height=>5, :uid=>"thumb-apotomo.png"}})
42 | # processed files must exist.
43 | exists?(original_metadata[:original][:uid]).must_equal true
44 | exists?(original_metadata[:thumb][:uid]).must_equal true
45 |
46 | # calling #process! again, with existing metadata, means OVERWRITING the existing image.
47 | task = Attachment.new(subject.metadata_hash).task(Pathname("test/fixtures/trb.png"))
48 | task.process!(:thumb) { |j| j.thumb!("48x48") }
49 |
50 | overwritten_metadata = task.metadata_hash
51 | overwritten_metadata.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}, :thumb=>{:width=>48, :height=>48, :uid=>"thumb-trb.png"}})
52 | # original must still exist.
53 | exists?(overwritten_metadata[:original][:uid]).must_equal true
54 | # old thumb must be deleted.
55 | exists?(original_metadata[:thumb][:uid]).must_equal false
56 | # new thumb must be existing
57 | exists?(overwritten_metadata[:thumb][:uid]).must_equal true
58 | end
59 |
60 | it do
61 | assert_raises Paperdragon::MissingUploadError do
62 | Attachment.new(nil).task.process!(:original)
63 | end
64 | end
65 |
66 | it do
67 | # after uploading "new", only delete when uid was in metadata.
68 | existing_metadata = {processing: true}
69 | metadata = Attachment.new(existing_metadata).task(logo).process!(:thumb)
70 | metadata.must_equal({:processing=>true, :thumb=>{:width=>216, :height=>63, :uid=>"thumb-apotomo.png"}})
71 | exists?("thumb-apotomo.png").must_equal true
72 | end
73 | end
74 |
75 |
76 | describe "#reprocess!" do
77 | let (:original) { Paperdragon::File.new("original/pic.jpg") }
78 |
79 | before do
80 | original.process!(logo) # original/uid exists.
81 | exists?(original.uid).must_equal true
82 | end
83 |
84 | subject { Attachment.new({
85 | :original=>{:uid=>"original/pic.jpg"}, :thumb=>{:uid=>"original/thumb.jpg"}, :bigger=>{:uid=>"original/bigger.jpg"}}).task
86 | }
87 |
88 | it do
89 | subject.reprocess!(:original, "1", original)
90 | subject.reprocess!(:thumb, "1", original) { |j| j.thumb!("16x16") }
91 |
92 | # FIXME: fingerprint should be added before .png suffix.
93 | subject.metadata_hash.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original/pic-1.jpg"}, :thumb=>{:width=>16, :height=>5, :uid=>"original/thumb-1.jpg"}, :bigger=>{:uid=>"original/bigger.jpg"}})
94 |
95 | # exists?(original.uri).must_equal false # deleted
96 | # exists?(new_uid).must_equal true
97 | end
98 |
99 | # don't pass in fingerprint+original.
100 | it do
101 | subject.reprocess!(:thumb) { |j| j.thumb!("24x24") }
102 | subject.reprocess!(:bigger) { |j| j.thumb!("48x48") }
103 |
104 | subject.metadata_hash.must_equal({:original=>{:uid=>"original/pic.jpg"}, :thumb=>{:width=>24, :height=>7, :uid=>"original/thumb-.jpg"}, :bigger=>{:uid=>"original/bigger-.jpg", :width=>48, :height=>14}})
105 | end
106 |
107 | # don't pass in original, this must NOT reload the original file more than once!
108 | it do
109 | subject.reprocess!(:thumb, 1) { |j| j.thumb!("24x24") }
110 | File.unlink("public/paperdragon/#{original.uid}") # removing means the original MUST be cached for next step.
111 | subject.reprocess!(:bigger, 1) { |j| j.thumb!("48x48") }
112 |
113 | subject.metadata_hash.must_equal(
114 | {:original=>{:uid=>"original/pic.jpg"}, :thumb=>{:width=>24, :height=>7, :uid=>"original/thumb-1.jpg"}, :bigger=>{:uid=>"original/bigger-1.jpg", :width=>48, :height=>14}})
115 | end
116 |
117 | # only process one, should return entire metadata hash
118 | it do
119 | subject.reprocess!(:thumb, "new") { |j| j.thumb!("24x24") }
120 | subject.metadata_hash.must_equal({:original=>{:uid=>"original/pic.jpg"}, :thumb=>{:width=>24, :height=>7, :uid=>"original/thumb-new.jpg"}, :bigger=>{:uid=>"original/bigger.jpg"}})
121 |
122 | # original must be unchanged
123 | exists?(Attachment.new(subject.metadata_hash)[:original].uid).must_equal true
124 | end
125 |
126 | # #rebuild_uid tries to replace existing fingerprint (default behaviour).
127 | it do
128 | subject.reprocess!(:thumb, "1234567890") { |j| j.thumb!("24x24") }
129 | metadata = subject.metadata_hash
130 | metadata.must_equal({:original=>{:uid=>"original/pic.jpg"}, :thumb=>{:width=>24, :height=>7, :uid=>"original/thumb-1234567890.jpg"}, :bigger=>{:uid=>"original/bigger.jpg"}})
131 |
132 | # this might happen in the next request.
133 | subject = Attachment.new(metadata).task
134 | subject.reprocess!(:thumb, "0987654321") { |j| j.thumb!("24x24") }
135 | subject.metadata_hash.must_equal({:original=>{:uid=>"original/pic.jpg"}, :thumb=>{:width=>24, :height=>7, :uid=>"original/thumb-0987654321.jpg"}, :bigger=>{:uid=>"original/bigger.jpg"}})
136 | end
137 |
138 | # #rebuild_uid eats integers.
139 | it { subject.reprocess!(:thumb, 1234081599) { |j| j.thumb!("24x24") }.must_equal({:original=>{:uid=>"original/pic.jpg"}, :thumb=>{:width=>24, :height=>7, :uid=>"original/thumb-1234081599.jpg"}, :bigger=>{:uid=>"original/bigger.jpg"}}) }
140 | end
141 |
142 |
143 | describe "#rename!" do
144 | before do
145 | attachment = Paperdragon::Attachment.new(nil)
146 | @upload_task = attachment.task(logo)
147 | metadata = @upload_task.process!(:original).must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}})
148 |
149 | # we do not update the attachment from task.
150 | attachment = Paperdragon::Attachment.new(@upload_task.metadata)
151 | exists?(attachment[:original].uid).must_equal true
152 | end
153 |
154 | let (:metadata) { @upload_task.metadata }
155 |
156 | # let (:subject) { Attachment.new(nil).task }
157 | it do
158 | attachment = Paperdragon::Attachment.new(metadata) # {:original=>{:width=>216, :height=>63, :uid=>"uid/original", :size=>9632}}
159 | task = attachment.task
160 | task.rename!(:original, "new") { |uid, new_uid|
161 | File.rename("public/paperdragon/"+uid, "public/paperdragon/"+new_uid)
162 | }.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo-new.png"}})
163 | end
164 | end
165 |
166 |
167 | describe "#delete!" do
168 | before do
169 | attachment = Paperdragon::Attachment.new(nil)
170 | @upload_task = attachment.task(logo)
171 | metadata = @upload_task.process!(:original).must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}})
172 |
173 | # we do not update the attachment from task.
174 | attachment = Paperdragon::Attachment.new(@upload_task.metadata)
175 | exists?(attachment[:original].uid).must_equal true
176 | end
177 |
178 | let (:metadata) { @upload_task.metadata }
179 |
180 | # #delete!(:original)
181 | it do
182 | attachment = Paperdragon::Attachment.new(metadata) # {:original=>{:width=>216, :height=>63, :uid=>"uid/original", :size=>9632}}
183 |
184 | attachment.task.delete!(:original).must_equal({})
185 |
186 | exists?(attachment[:original].uid).must_equal false
187 | end
188 |
189 | # #delete no args.
190 | it do
191 | metadata = @upload_task.process!(:thumb)
192 | metadata.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}, :thumb=>{:width=>216, :height=>63, :uid=>"thumb-apotomo.png"}})
193 |
194 | attachment = Paperdragon::Attachment.new(metadata) # {:original=>{:width=>216, :height=>63, :uid=>"uid/original", :size=>9632}}
195 | # thumb and original are both there.
196 | exists?(attachment[:original].uid).must_equal true
197 | exists?(attachment[:thumb].uid).must_equal true
198 |
199 | attachment.task.delete!.must_equal({})
200 |
201 | exists?(attachment[:original].uid).must_equal false
202 | exists?(attachment[:thumb].uid).must_equal false
203 | end
204 | end
205 | end
206 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | require 'paperdragon'
2 | require 'minitest/autorun'
3 |
4 | MiniTest::Spec.class_eval do
5 | def exists?(uid)
6 | File.exists?("public/paperdragon/" + uid)
7 | end
8 |
9 | def generate_uid
10 | Dragonfly.app.datastore.send(:relative_path_for, "aptomo.png")
11 | end
12 |
13 | def self.it(name=nil, *args)
14 | name ||= Random.rand
15 | super
16 | end
17 | end
--------------------------------------------------------------------------------