paperdragon 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a8aa8c4f218850c6ed7a5050642db8351df76d47
4
- data.tar.gz: 04d4f324dfc84ebd85a42429555dc9ad2bd4d72c
3
+ metadata.gz: f827f261f58ce5b096178c2b90010e90a99b8e27
4
+ data.tar.gz: f6731a32d9779faab6b1f83da6df1006acab51a4
5
5
  SHA512:
6
- metadata.gz: 28d1195ff612e638e39a8a7a26eb2f8e96b2bf0090e6d11dbdba1a81cf3a8a269f803786f9999677aadc7487d0b52da745bc8b361050e4f56b5901818a86dd04
7
- data.tar.gz: b8b01e524efaa05b65c428cf1aeb47083ef880253bff8992786d8ecb9b0887a2d1e5186ece278260183b6b2fa51069048ae23508a35dd827e41b5b3744bb7b42
6
+ metadata.gz: 4481129325b2fa60b3034ae07e1cb4a9e636996f68fc4697c03356863e1e25136f272ae925c12e37ac09b7bff94ebd509d548fa07783df38c957a54e587234ee
7
+ data.tar.gz: a39824b4503ef45240a09a80845457194504b954cfe66ee4a865207589f5000be7d57dccbceddded4b328b6c2fb0cfaa387e62974999d2c3e8b7f465155a3866
data/README.md CHANGED
@@ -8,22 +8,64 @@ Add this line to your application's Gemfile:
8
8
 
9
9
  gem 'paperdragon'
10
10
 
11
- And then execute:
12
11
 
13
- $ bundle
12
+ Paperdragon is completely decoupled from ActiveRecord. Attachment-related calls are delegated to paperdragon objects, the model is solely used for persisting file UIDs.
14
13
 
15
- Or install it yourself as:
14
+ Where Paperclip or Carrierwave offer you a handy DSL to configure the processing, Paperdragon comes with an API. You _program_ what you wanna do. This is only a tiny little bit more code and gives you complete control over the entire task.
16
15
 
17
- $ gem install paperdragon
18
16
 
19
- ## Usage
20
17
 
21
- TODO: Write usage instructions here
18
+ The goal is to make you _understand_ what is going on.
22
19
 
23
- ## Contributing
20
+ * you control processing and storage, e.g. first thumbnails and cropping, then process the rest. easy to sidekiq.
21
+ error handling
22
+ UID generation handled by you. also, updating (e.g. new fingerprint)
23
+ * only process subset, e.g. in test.
24
24
 
25
- 1. Fork it ( https://github.com/[my-github-username]/paperdragon/fork )
26
- 2. Create your feature branch (`git checkout -b my-new-feature`)
27
- 3. Commit your changes (`git commit -am 'Add some feature'`)
28
- 4. Push to the branch (`git push origin my-new-feature`)
29
- 5. Create a new Pull Request
25
+
26
+ File
27
+
28
+ All process methods return Metadata hash
29
+ yield Job, save it from the block if you need it
30
+ override #default_metadata_for when you wanna change it
31
+ last arg in process method gets merged into metadata hash
32
+
33
+ Design
34
+ Operations in File look like scripts per design. I could have abstracted various steps into higher level methods, however, as file processing _is_ a lot scripting, I decided to sacrifice redundancy for better understandable code.
35
+
36
+
37
+ Paperclip Compatibility
38
+
39
+ 1. Stores file to same location as paperclip would do.
40
+ 2. `Photo#url` will return the same URL as paperclip.
41
+ 3. P::Model image.url(:thumb) still works, your rendering code will still work.
42
+ 4. Cleaner API for generating URLs. For example, we needed to copy images from production to staging. With paperclip, it was impossible to create paths for both environments.
43
+
44
+ Paperclip uses several columns to compute the UID. Once this is done, it doesn't store that UID in the database but updates the respective fields, which makes it a bit awkward to maintain.
45
+
46
+ Paperdragon simply dumps the image uid along with meta data into image_meta_data.
47
+
48
+ You have to take care of updating image_fingerprint etc yourself when changing stuff and still using paperclip to compute urls.
49
+
50
+
51
+
52
+
53
+ Original paperclip UID:
54
+ it { pic.image(:original).should == "/system/test/pics/images/002/216/376/bc7b26d983db8ced792e38f0c34aba417f75c2e7_key/original-c5b7e624adc5b67e13435baf26e65bc8-1399980114/DSC_4876.jpg" }
55
+
56
+ 1) Uid
57
+ Failure/Error: should == "system/test/pics/images/002/216/376/bc7b26d983db8ced792e38f0c34aba417f75c2e7_key/original-c5b7e624adc5b67e13435baf26e65bc8-1399980114/DSC_4876.jpg" }
58
+ expected: "system/test/pics/images/002/216/376/bc7b26d983db8ced792e38f0c34aba417f75c2e7_key/original-c5b7e624adc5b67e13435baf26e65bc8-1399980114/DSC_4876.jpg"
59
+ got: "system/test/pics/images/002/216/376/bc7b26d983db8ced792e38f0c34aba417f75c2e7_key/original/DSC_4876.jpg" (using ==)
60
+
61
+ Feel like a hacker reverse-engineering
62
+
63
+ ## Rails
64
+
65
+ Dragonfly.app.configure do
66
+ plugin :imagemagick
67
+
68
+ datastore :file,
69
+ :server_root => 'public',
70
+ :root_path => 'public/images'
71
+ end
data/Rakefile CHANGED
@@ -1,2 +1,9 @@
1
1
  require "bundler/gem_tasks"
2
+ require "rake/testtask"
2
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
data/lib/paperdragon.rb CHANGED
@@ -1,5 +1,13 @@
1
1
  require "paperdragon/version"
2
+ require 'dragonfly'
2
3
 
3
4
  module Paperdragon
4
- # Your code goes here...
5
+ class MissingUploadError < RuntimeError
6
+ end
5
7
  end
8
+
9
+ require 'paperdragon/file'
10
+ require 'paperdragon/metadata'
11
+ require 'paperdragon/attachment'
12
+ require 'paperdragon/task'
13
+ require 'paperdragon/model'
@@ -0,0 +1,80 @@
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.new(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
+ def rebuild_uid(file, fingerprint=nil) # the signature of this method is to be considered semi-private.
40
+ ext = ::File.extname(file.uid)
41
+ name = ::File.basename(file.uid, ext)
42
+ file.uid.sub(name, "#{name}#{fingerprint}")
43
+ end
44
+
45
+ def exists? # should be #uploaded? or #stored?
46
+ # not sure if i like that kind of state here, so consider method semi-public.
47
+ @metadata.populated?
48
+ end
49
+
50
+ private
51
+ attr_reader :options
52
+
53
+ # Computes UID when File doesn't have one, yet. Called in #initialize.
54
+ def uid_from(*args)
55
+ build_uid(*args)
56
+ end
57
+
58
+ def build_uid(style, file)
59
+ # can we use Dragonfly's API here?
60
+ "#{style}-#{Dragonfly::TempObject.new(file).original_filename}"
61
+ end
62
+ end
63
+
64
+
65
+ module SanitizeUid
66
+ def uid_from(*args)
67
+ sanitize(super)
68
+ end
69
+
70
+ def sanitize(uid)
71
+ #URI::encode(uid) # this is wrong, we can't send %21 in path to S3!
72
+ uid.gsub(/(#|\?)/, "_") # escape # and ?, only.
73
+ end
74
+ end
75
+
76
+
77
+ include InstanceMethods
78
+ include SanitizeUid # overrides #uid_from.
79
+ end
80
+ end
@@ -0,0 +1,46 @@
1
+ module Paperdragon
2
+ # A physical file with a UID.
3
+ class File
4
+ def initialize(uid, options={})
5
+ @uid = uid
6
+ @options = options
7
+ @data = nil # DISCUSS: do we need that here?
8
+ end
9
+
10
+ attr_reader :uid, :options
11
+ alias_method :metadata, :options
12
+
13
+ def url(opts={})
14
+ Dragonfly.app.remote_url_for(uid, opts)
15
+ end
16
+
17
+ def data
18
+ puts "........................FETCH (data): #{uid}, #{@data ? :cached : (:fetching)}"
19
+ @data ||= Dragonfly.app.fetch(uid).data
20
+ end
21
+
22
+ # attr_reader :meta_data
23
+
24
+ require 'paperdragon/file/operations'
25
+ include Process
26
+ include Delete
27
+ include Reprocess
28
+ include Rename
29
+
30
+
31
+ private
32
+ # replaces the UID.
33
+ def uid!(new_uid)
34
+ @uid = new_uid
35
+ end
36
+
37
+ # Override if you want to include/exclude properties in this file metadata.
38
+ def default_metadata_for(job)
39
+ {:width => job.width, :height => job.height, :uid => uid}#, :content_type => job.mime_type}
40
+ end
41
+
42
+ def metadata_for(job, additional={})
43
+ default_metadata_for(job).merge(additional)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,68 @@
1
+ module Paperdragon
2
+ class File
3
+ # DISCUSS: allow the metadata passing here or not?
4
+ module Process
5
+ def process!(file, metadata={})
6
+ job = Dragonfly.app.new_job(file)
7
+
8
+ yield job if block_given?
9
+
10
+ puts "........................STORE (process): #{uid}"
11
+ job.store(path: uid, :headers => {'x-amz-acl' => 'public-read', "Content-Type" => "image/jpeg"})
12
+
13
+ @data = nil
14
+ metadata_for(job, metadata)
15
+ end
16
+ end
17
+
18
+
19
+ module Delete
20
+ def delete!
21
+ puts "........................DELETE (delete): #{uid}"
22
+ Dragonfly.app.destroy(uid)
23
+ end
24
+ end
25
+
26
+
27
+ module Reprocess
28
+ def reprocess!(fingerprint, original, metadata={})
29
+ job = Dragonfly.app.new_job(original.data) # inheritance here somehow?
30
+
31
+ yield job if block_given?
32
+
33
+ old_uid = uid
34
+ uid!(fingerprint) # new UID is computed and set.
35
+
36
+ puts "........................STORE (reprocess): #{uid}"
37
+ job.store(path: uid, headers: {'x-amz-acl' => 'public-read', "Content-Type" => "image/jpeg"}) # store with thumb url.
38
+
39
+ puts "........................DELETE (reprocess): #{old_uid}"
40
+ Dragonfly.app.destroy(old_uid)
41
+
42
+ @data = nil
43
+ metadata_for(job, metadata)
44
+ end
45
+ end
46
+
47
+
48
+ module Rename
49
+ def rename!(fingerprint, metadata={}) # fixme: we are currently ignoring the custom metadata.
50
+ old_uid = uid
51
+ uid!(fingerprint)
52
+
53
+ puts "........................MV:
54
+ #{old_uid}
55
+ #{uid}"
56
+ # dragonfly_s3 = Dragonfly.app.datastore
57
+ # 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"})
58
+ yield old_uid, uid
59
+
60
+ puts "........................DELETE: #{old_uid}"
61
+ Dragonfly.app.destroy(old_uid)
62
+
63
+
64
+ self.metadata.merge(:uid => uid) # usually, metadata is already set to the old metadata when File was created via Attachment.
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,31 @@
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
7
+ def initialize(hash)
8
+ @hash = hash || {}
9
+ end
10
+
11
+ def [](name)
12
+ @hash[name] || {}
13
+ end
14
+
15
+ def populated?
16
+ @hash.size > 0
17
+ end
18
+
19
+ def merge!(hash)
20
+ @hash.merge!(hash)
21
+ end
22
+
23
+ def dup
24
+ self.class.new(@hash.dup)
25
+ end
26
+
27
+ def to_hash
28
+ @hash
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ module Paperdragon
2
+ module Model
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def processable(name, attachment_class=Attachment)
9
+ include attachment_accessor_for(name, attachment_class)
10
+ end
11
+
12
+ private
13
+ # Creates Avatar#image that returns a Paperdragon::File instance.
14
+ def attachment_accessor_for(name, attachment_class)
15
+ mod = Module.new do # TODO: abstract that into Uber, we use it everywhere.
16
+ define_method name do
17
+ attachment_class.new(self.image_meta_data, {:model => self})
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,68 @@
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'
@@ -0,0 +1,40 @@
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(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(model, attachment_class)
27
+ @attachment = attachment_class.new(model.image_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)
35
+ @attachment.send(name, *args)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,51 @@
1
+ module Paperdragon
2
+ # Gives a simple API for processing multiple versions of a single attachment.
3
+ class Task
4
+ def initialize(attachment, upload=nil)
5
+ @attachment = attachment
6
+ @upload = upload
7
+ @metadata = attachment.metadata.dup # DISCUSS: keep this dependency?
8
+
9
+ yield self if block_given?
10
+ end
11
+
12
+ attr_reader :metadata
13
+ def metadata_hash # semi-private, might be removed.
14
+ metadata.to_hash
15
+ end
16
+
17
+ # process!(style, [*args,] &block) :
18
+ # version = CoverGirl::Photo.new(@model, style, *args)
19
+ # metadata = version.process!(upload, &block)
20
+ # merge! {style => metadata}
21
+ def process!(style, &block)
22
+ @metadata.merge!(style => file(style, upload).process!(upload, &block))
23
+ end
24
+
25
+ # fingerprint optional => filename is gonna remain the same
26
+ # original nil => use [:original]
27
+ def reprocess!(style, fingerprint=nil, original=nil, &block)
28
+ original ||= file(:original)
29
+ version = file(style)
30
+ new_uid = @attachment.rebuild_uid(version, fingerprint)
31
+
32
+ @metadata.merge!(style => version.reprocess!(new_uid, original, &block))
33
+ end
34
+
35
+ def rename!(style, fingerprint, &block)
36
+ version = file(style)
37
+ new_uid = @attachment.rebuild_uid(version, fingerprint)
38
+
39
+ @metadata.merge!(style => version.rename!(new_uid, &block))
40
+ end
41
+
42
+ private
43
+ def file(style, upload=nil)
44
+ @attachment[style, upload]
45
+ end
46
+
47
+ def upload
48
+ @upload or raise MissingUploadError.new("You called #process! but didn't pass an uploaded file to Attachment#task.")
49
+ end
50
+ end
51
+ end
@@ -1,3 +1,3 @@
1
1
  module Paperdragon
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/paperdragon.gemspec CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Paperdragon::VERSION
9
9
  spec.authors = ["Nick Sutterer"]
10
10
  spec.email = ["apotonick@gmail.com"]
11
- spec.summary = %q{Simple but explicit dragonfly attachments.}
12
- spec.description = %q{Simple but explicit dragonfly attachments.}
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
13
  spec.homepage = ""
14
14
  spec.license = "MIT"
15
15
 
@@ -18,6 +18,10 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+ spec.add_dependency "dragonfly"
22
+ spec.add_dependency "uber"
23
+
21
24
  spec.add_development_dependency "bundler", "~> 1.6"
22
25
  spec.add_development_dependency "rake"
26
+ spec.add_development_dependency "minitest"
23
27
  end
@@ -0,0 +1,91 @@
1
+ require 'test_helper'
2
+
3
+ class AttachmentSpec < MiniTest::Spec
4
+ class Attachment < Paperdragon::Attachment
5
+ private
6
+ def uid_from(style)
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
data/test/file_test.rb ADDED
@@ -0,0 +1,138 @@
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 <= 457 # smaller after thumb!
55
+ end
56
+
57
+ # additional metadata
58
+ it do
59
+ file.process!(logo, :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 <= 457
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
Binary file
@@ -0,0 +1,54 @@
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.new(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.new(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.new({}) }
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.new(original).dup.merge!(:additional => {})
46
+ original[:additional].must_equal nil
47
+ end
48
+
49
+ # #merge!
50
+ it { Paperdragon::Metadata.new(original).merge!(:additional => {}).to_hash.must_equal({:original=>{}, :additional=>{}}) }
51
+
52
+ # #to_hash
53
+ it { Paperdragon::Metadata.new(original).to_hash.must_equal({:original=>{}}) }
54
+ end
@@ -0,0 +1,35 @@
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
+
15
+
16
+ def image_meta_data
17
+ {:thumb => {:uid => "Avatar-thumb"}}
18
+ end
19
+ end
20
+
21
+ it { Avatar.new.image[:thumb].url.must_equal "/paperdragon/Avatar-thumb" }
22
+
23
+
24
+ # minimum setup
25
+ class Image
26
+ include Paperdragon::Model
27
+ processable :image
28
+
29
+ def image_meta_data
30
+ {:thumb => {:uid => "Avatar-thumb"}}
31
+ end
32
+ end
33
+
34
+ it { Image.new.image[:thumb].url.must_equal "/paperdragon/Avatar-thumb" }
35
+ end
@@ -0,0 +1,57 @@
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").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
+ include Paperdragon::Paperclip::Model
41
+ processable :image, Attachment
42
+
43
+
44
+ def image_meta_data
45
+ {:thumb => {:uid => "Avatar-thumb"}}
46
+ end
47
+ end
48
+
49
+ # old paperclip style
50
+ it { Avatar.new.image.url(:thumb).must_equal "/paperdragon/Avatar-thumb" }
51
+
52
+ # paperdragon style
53
+ it { Avatar.new.image[:thumb].url.must_equal "/paperdragon/Avatar-thumb" }
54
+
55
+ # delegates all unknown methods back to Attachment.
56
+ it { Avatar.new.image.exists?.must_equal "Of course!" }
57
+ end
data/test/task_test.rb ADDED
@@ -0,0 +1,105 @@
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
+ end
22
+
23
+ # task without block
24
+ let (:subject) { Attachment.new(nil).task(logo) }
25
+
26
+ describe "#process!" do
27
+ it do
28
+ subject.process!(:original)
29
+ subject.process!(:thumb) { |j| j.thumb!("16x16") }
30
+
31
+ subject.metadata_hash.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}, :thumb=>{:width=>16, :height=>5, :uid=>"thumb-apotomo.png"}})
32
+ end
33
+
34
+ it do
35
+ assert_raises Paperdragon::MissingUploadError do
36
+ Attachment.new(nil).task.process!(:original)
37
+ end
38
+ end
39
+ end
40
+
41
+
42
+ describe "#reprocess!" do
43
+ let (:original) { Paperdragon::File.new("original/pic.jpg") }
44
+
45
+ before do
46
+ original.process!(logo) # original/uid exists.
47
+ exists?(original.uid).must_equal true
48
+ end
49
+
50
+ subject { Attachment.new({
51
+ :original=>{:uid=>"original/pic.jpg"}, :thumb=>{:uid=>"original/thumb.jpg"}}).task
52
+ }
53
+
54
+ # FIXME: fingerprint should be added before .png suffix, idiot!
55
+ it do
56
+ subject.reprocess!(:original, "-1", original)
57
+ subject.reprocess!(:thumb, "-1", original) { |j| j.thumb!("16x16") }
58
+
59
+ # it
60
+ 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"}})
61
+ # it
62
+ # exists?(original.uri).must_equal false # deleted
63
+ # exists?(new_uid).must_equal true
64
+ end
65
+
66
+ # don't pass in fingerprint+original.
67
+ it do
68
+ subject.reprocess!(:thumb) { |j| j.thumb!("24x24") }
69
+ subject.metadata_hash.must_equal({:original=>{:uid=>"original/pic.jpg"}, :thumb=>{:width=>24, :height=>7, :uid=>"original/thumb.jpg"}})
70
+ end
71
+
72
+ # only process one, should return entire metadata hash
73
+ it do
74
+ subject.reprocess!(:thumb, "-new") { |j| j.thumb!("24x24") }
75
+ subject.metadata_hash.must_equal({:original=>{:uid=>"original/pic.jpg"}, :thumb=>{:width=>24, :height=>7, :uid=>"original/thumb-new.jpg"}})
76
+
77
+ # original must be unchanged
78
+ exists?(Attachment.new(subject.metadata_hash)[:original].uid).must_equal true
79
+ end
80
+ end
81
+
82
+
83
+ describe "#rename!" do
84
+ before do
85
+ attachment = Paperdragon::Attachment.new(nil)
86
+ @upload_task = attachment.task(logo)
87
+ metadata = @upload_task.process!(:original).must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo.png"}})
88
+
89
+ # we do not update the attachment from task.
90
+ attachment = Paperdragon::Attachment.new(@upload_task.metadata)
91
+ exists?(attachment[:original].uid).must_equal true
92
+ end
93
+
94
+ let (:metadata) { @upload_task.metadata }
95
+
96
+ # let (:subject) { Attachment.new(nil).task }
97
+ it do
98
+ attachment = Paperdragon::Attachment.new(metadata) # {:original=>{:width=>216, :height=>63, :uid=>"uid/original", :size=>9632}}
99
+ task = attachment.task
100
+ task.rename!(:original, "-new") { |uid, new_uid|
101
+ File.rename("public/paperdragon/"+uid, "public/paperdragon/"+new_uid)
102
+ }.must_equal({:original=>{:width=>216, :height=>63, :uid=>"original-apotomo-new.png"}})
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,12 @@
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
+ end
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paperdragon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Sutterer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-17 00:00:00.000000000 Z
11
+ date: 2014-08-25 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dragonfly
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: uber
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: bundler
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -38,7 +66,21 @@ dependencies:
38
66
  - - ">="
39
67
  - !ruby/object:Gem::Version
40
68
  version: '0'
41
- description: Simple but explicit dragonfly attachments.
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Explicit image processing based on Dragonfly with Paperclip compatibility.
42
84
  email:
43
85
  - apotonick@gmail.com
44
86
  executables: []
@@ -51,8 +93,24 @@ files:
51
93
  - README.md
52
94
  - Rakefile
53
95
  - lib/paperdragon.rb
96
+ - lib/paperdragon/attachment.rb
97
+ - lib/paperdragon/file.rb
98
+ - lib/paperdragon/file/operations.rb
99
+ - lib/paperdragon/metadata.rb
100
+ - lib/paperdragon/model.rb
101
+ - lib/paperdragon/paperclip.rb
102
+ - lib/paperdragon/paperclip/model.rb
103
+ - lib/paperdragon/task.rb
54
104
  - lib/paperdragon/version.rb
55
105
  - paperdragon.gemspec
106
+ - test/attachment_test.rb
107
+ - test/file_test.rb
108
+ - test/fixtures/apotomo.png
109
+ - test/metadata_test.rb
110
+ - test/model_test.rb
111
+ - test/paperclip_uid_test.rb
112
+ - test/task_test.rb
113
+ - test/test_helper.rb
56
114
  homepage: ''
57
115
  licenses:
58
116
  - MIT
@@ -76,6 +134,14 @@ rubyforge_project:
76
134
  rubygems_version: 2.2.1
77
135
  signing_key:
78
136
  specification_version: 4
79
- summary: Simple but explicit dragonfly attachments.
80
- test_files: []
137
+ summary: Explicit image processing based on Dragonfly with Paperclip compatibility.
138
+ test_files:
139
+ - test/attachment_test.rb
140
+ - test/file_test.rb
141
+ - test/fixtures/apotomo.png
142
+ - test/metadata_test.rb
143
+ - test/model_test.rb
144
+ - test/paperclip_uid_test.rb
145
+ - test/task_test.rb
146
+ - test/test_helper.rb
81
147
  has_rdoc: