paperdragon 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  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: