nozzle 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 574bb2f5aafe5e663ec50b3237cacef48010aeab
4
+ data.tar.gz: 0b66db6e1d494592209504e2fdd92c94c07ffa83
5
+ SHA512:
6
+ metadata.gz: a8054882ea0598df4fefabf947fdb5d512ff6d9d135a76c1ecd734fb92f70440a0eb223ca719a11f182df019a43cfbd44615ee2e9cf085244a863a5bc15d0c97
7
+ data.tar.gz: 7489babd43a467689511eea8d175dfab7107e44b0af0236e5d4efd0da480427b159992df58f732016b42726eb62474aa861963e62083ca2970d7021b6eb789ca
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .rbx
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in nozzle.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 ujifgc
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.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ [![Build Status](https://travis-ci.org/ujifgc/nozzle.png)](https://travis-ci.org/ujifgc/nozzle)
2
+ [![Code Climate](https://codeclimate.com/github/ujifgc/nozzle.png)](https://codeclimate.com/github/ujifgc/nozzle)
3
+
4
+ # Nozzle
5
+
6
+ A gem to store and serve attachments for ruby rack applications
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'nozzle'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install nozzle
21
+
22
+ ## Usage
23
+
24
+ TODO: Write usage instructions here
25
+
26
+ ## Contributing
27
+
28
+ 1. Fork it
29
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
30
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
31
+ 4. Push to the branch (`git push origin my-new-feature`)
32
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << "test"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ t.verbose = true
9
+ end
10
+
11
+ task :default => :test
@@ -0,0 +1,246 @@
1
+ require 'fileutils'
2
+ require 'tempfile'
3
+ require 'nozzle/adapter/outlet'
4
+
5
+ module Nozzle
6
+ module Adapter
7
+ class Base
8
+ include Nozzle::Adapter::Outlet
9
+
10
+ # Initializes internal structure of new adapter.
11
+ # outlet_class.new( instance, :avatar, 'image.jpg', :fake => true )
12
+ def initialize( record, column, filename, options = {} )
13
+ @record = record
14
+ @model = record.class
15
+ @column = column.to_sym
16
+ @filename = filename
17
+ settings.merge! options
18
+ end
19
+
20
+ # Sets or gets settings provided by options.
21
+ # if instance.avatar.settings[:fake]
22
+ # instance.avatar.settings[:fake] = false
23
+ # end
24
+ def settings
25
+ @settings ||= {}
26
+ end
27
+
28
+ # Constructs an URL which relatively points to the file.
29
+ # instance.avatar.url # => '/uploads/Model/avatar/image.jpg'
30
+ # How it's constructed:
31
+ # "#{public_path}/#{filename}"
32
+ # "/#{adapter_folder}/#{relative_folder}/#{filename}"
33
+ # "/uploads/#{@model}/#{@column}/#{filename}"
34
+ # "/uploads/Model/avatar/image.jpg"
35
+ # Note: if filename is not yet stored, +default_url+ is called.
36
+ def url
37
+ File.join '', public_path, filename
38
+ rescue TypeError
39
+ default_url
40
+ end
41
+
42
+ # Constructs a filesustem path which absolutely points to stored file.
43
+ # instance.avatar.path # => 'public/uploads/Model/avatar/image.jpg'
44
+ # How it's constructed:
45
+ # "/#{system_path}/#{filename}"
46
+ # "/#{adapter_path}/#{relative_folder}/#{filename}"
47
+ # "#{root}/#{adapter_folder}/#{@model}/#{@column}/#{filename}"
48
+ # "public/uploads/#{@model}/#{@column}/#{filename}"
49
+ # "public/uploads/Model/avatar/image.jpg"
50
+ # Note: if filename is not yet stored, nil is returned.
51
+ def path
52
+ File.join system_path, filename
53
+ rescue TypeError
54
+ nil
55
+ end
56
+
57
+ # Returns intermediate path to the tempfile if the record is not yet
58
+ # saved and file is not yet stored at path.
59
+ def access_path
60
+ @tempfile_path || path
61
+ end
62
+
63
+ # Returns file content_type stored in avatar_content_type column
64
+ # of the record.
65
+ def content_type
66
+ @record.send( :"#{@column}_content_type" ) rescue ''
67
+ end
68
+
69
+ # Returns file size stored in avatar_size column of the record.
70
+ def size
71
+ @record.send( :"#{@column}_size" ) rescue -1
72
+ end
73
+
74
+ # Returns stored filename.
75
+ # instance.avatar.filename # => 'image.jpg'
76
+ def filename
77
+ @filename
78
+ end
79
+
80
+ # Returns nil.
81
+ # This SHOULD be overridden by subclasses of Nozzle::Adapter::Base.
82
+ # instance.avatar.default_url # => nil
83
+ def default_url
84
+ nil
85
+ end
86
+
87
+ # Returns root path of application's static assets.
88
+ # instance.avatar.root # => 'public'
89
+ # This MAY be overridden to return an application root different
90
+ # from the current folder.
91
+ def root
92
+ 'public'
93
+ end
94
+
95
+ # Returns folder name of the adapter relative to application public.
96
+ # instance.avatar.adapter_folder # => 'uploads'
97
+ # This MAY be overridden to specify where all the files should be stored.
98
+ def adapter_folder
99
+ 'uploads'
100
+ end
101
+
102
+ # Returns filesystem folder path of the adapter relative to adapter root.
103
+ # instance.avatar.adapter_path # => 'public/uploads'
104
+ # It is constructed from #root, 'public' and #adapter_folder.
105
+ def adapter_path
106
+ File.join root, adapter_folder
107
+ end
108
+
109
+ # Returns file's folder relative to #adapter_path.
110
+ # instance.avatar.relative_folder # => 'Model/avatar'
111
+ # It is constructed from object's class name and column name.
112
+ # This MAY be overridden to place files somwhere other than 'Model/avatar'.
113
+ def relative_folder
114
+ File.join @model.to_s, @column.to_s
115
+ end
116
+
117
+ # Returns filesystem folder path relative to adapter root.
118
+ # instance.avatar.system_path # => 'public/uploads/Model/avatar'
119
+ # It is constructed from #adapter_path and #relative_folder
120
+ def system_path
121
+ File.join adapter_path, relative_folder
122
+ end
123
+
124
+ # Returns folder path relative to public folder.
125
+ # instance.avatar.public_path # => 'uploads/Model/avatar'
126
+ # It is constructed from #adapter_folder and #relative_folder
127
+ def public_path
128
+ File.join adapter_folder, relative_folder
129
+ end
130
+
131
+ # Inspects class name and url of the object.
132
+ # instance.avatar.to_s # => 'Model#url: /uploads/Model/avatar/image.jpg'
133
+ def to_s
134
+ "#{self.class}#url: #{url}"
135
+ end
136
+
137
+ # Sets adapter to delete stored file on #adapеr_after_save.
138
+ # instance.avatar.delete # => nil
139
+ def delete
140
+ @record.send(:"#{@column}=", nil)
141
+ end
142
+
143
+ # Returns adapter instance.
144
+ # It's used in Nozzle::Adapter#avatar after retrieving filename from the object.
145
+ def load( value )
146
+ @filename = value
147
+ self
148
+ end
149
+
150
+ # Fills internal structure of the adapter with new file's path.
151
+ # It's used in Nozzle::Adapter#avatar= before sending filename to the object.
152
+ def dump( value )
153
+ reset
154
+ @original_path = path
155
+ return nil unless value
156
+
157
+ new_path = expand_argument value
158
+ raise Errno::ENOENT, "'#{new_path}'" unless File.exists?(new_path)
159
+
160
+ @tempfile_path = File.expand_path(new_path)
161
+ detect_properties
162
+ @filename
163
+ end
164
+
165
+ # Stores temporary filename by the constructed path. Deletes old file.
166
+ # Note: the file is moved if it's path contains /tmp/ or /temp/, copied
167
+ # otherwise.
168
+ def store!
169
+ unlink! @original_path
170
+ return nil unless @tempfile_path
171
+
172
+ new_path = path
173
+ FileUtils.mkdir_p File.dirname(new_path)
174
+ result = if @tempfile_path =~ /\/te?mp\//
175
+ FileUtils.move @tempfile_path, new_path
176
+ else
177
+ FileUtils.copy @tempfile_path, new_path
178
+ end
179
+ File.chmod 0644, new_path
180
+ reset
181
+ result
182
+ end
183
+
184
+ # Deletes file by path. Do not use, it will break adapter's integrity.
185
+ # It's called in #avatar_after_destroy after the object is destroyed.
186
+ # unlink! # deletes path
187
+ # unlink! @original_path # deletes @original_path
188
+ # unlink! nil # deletes nothing
189
+ def unlink!( target = path )
190
+ delete_file_and_folder! target if target
191
+ end
192
+
193
+ def as_json
194
+ { :url => url }
195
+ end
196
+
197
+ private
198
+
199
+ # Tries to detect content_type and size of the file.
200
+ # Note: this method calls `file` system command to detect file content type.
201
+ def detect_properties
202
+ @record.send( :"#{@column}_content_type=", `file -bp --mime-type '#{access_path}'`.to_s.strip )
203
+ @record.send( :"#{@column}_size=", File.size(access_path) )
204
+ rescue NoMethodError
205
+ nil
206
+ end
207
+
208
+ # Resets internal paths.
209
+ def reset
210
+ @original_path = nil
211
+ @tempfile_path = nil
212
+ end
213
+
214
+ # Analyzes the value assigned to adapter and fills @filename. Returns
215
+ # system path where temporary file is located.
216
+ # The +value+ MUST be File, String, Hash or nil. See Nozzle::Adapter#avatar=.
217
+ def expand_argument( value )
218
+ tempfile_path = case value
219
+ when String
220
+ value
221
+ when File, Tempfile
222
+ value.path
223
+ when Hash
224
+ expand_argument( value[:tempfile] || value['tempfile'] )
225
+ else
226
+ raise ArgumentError, "#{@model}##{@column}= argument must be kind of String, File, Tempfile or Hash[:tempfile => 'path']"
227
+ end
228
+ @filename = value.kind_of?(Hash) && ( value[:filename] || value['filename'] ) || File.basename(tempfile_path)
229
+ tempfile_path
230
+ end
231
+
232
+ # Deletes the specified file and all empty folders recursively stopping at
233
+ # #adapter_folder.
234
+ def delete_file_and_folder!( file_path )
235
+ FileUtils.rm_f file_path
236
+ boundary = adapter_path + '/'
237
+ loop do
238
+ file_path = File.dirname file_path
239
+ break unless file_path.index boundary
240
+ FileUtils.rmdir file_path
241
+ end
242
+ end
243
+
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,27 @@
1
+ require 'nozzle/adapter/base'
2
+
3
+ module Nozzle
4
+ module Adapter
5
+ class Image < Nozzle::Adapter::Base
6
+ DEFAULT_SETTINGS = {
7
+ :thumb_size => '200x150',
8
+ }.freeze
9
+
10
+ def initialize( record, column, filename = nil, options = {} )
11
+ @settings = DEFAULT_SETTINGS.dup
12
+ super( record, column, filename, options )
13
+ end
14
+
15
+ def default_url
16
+ '/images/image_missing.png'
17
+ end
18
+
19
+ outlet :thumb do
20
+ def prepare( original, result )
21
+ `convert #{original} -thumbnail #{settings[:thumb_size]} #{result}`
22
+ end
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,80 @@
1
+ module Nozzle
2
+ module Adapter
3
+ module Outlet
4
+
5
+ def self.included(base)
6
+ base.instance_eval do
7
+ def outlets
8
+ @outlets ||= {}
9
+ end
10
+ end
11
+ base.extend(ClassMethods)
12
+ end
13
+
14
+ def outlets
15
+ return @outlets if @outlets
16
+ @outlets = {}
17
+ self.class.outlets.each do |name, outlet_class|
18
+ @outlets[name] = outlet_class.new(@record, @column, @filename, @settings)
19
+ end
20
+ @outlets
21
+ end
22
+
23
+ # Copies the file from original path to this outlet path.
24
+ # prepare( original, result )
25
+ # This method SHOULD be overridden in the outlet block. Example:
26
+ # class NewAdapter < Nozzle::Adapter::Base
27
+ # outlet :thumb do
28
+ # def prepare( original, result )
29
+ # `convert #{original} -thumbnail x96 #{result}`
30
+ # end
31
+ # end
32
+ # end
33
+ # In the example system +convert+ is called to resize the original file
34
+ # and save it's smaller version in result path.
35
+ def prepare( original, result )
36
+ FileUtils.mkdir_p File.dirname(result)
37
+ FileUtils.cp original, result
38
+ end
39
+
40
+ def prepare!
41
+ prepare( @record.send(@column).path, path )
42
+ end
43
+
44
+ def cleanup!
45
+ delete_file_and_folder!( path ) if respond_to?(:version_name)
46
+ outlets.each{ |name, outlet| outlet.cleanup! }
47
+ end
48
+
49
+ module ClassMethods
50
+
51
+ def outlet( name, &block )
52
+ class_eval <<-RUBY,__FILE__,__LINE__+1
53
+ def #{name}
54
+ outlets[:#{name}]
55
+ end
56
+ RUBY
57
+ outlets[name] = create_outlet( name, &block )
58
+ end
59
+
60
+ private
61
+
62
+ def create_outlet( name, &block )
63
+ new_outlet = Class.new(self)
64
+ new_outlet.class_eval <<-RUBY,__FILE__,__LINE__+1
65
+ def version_name
66
+ (defined?(super) ? super+'_' : '') + "#{name}"
67
+ end
68
+ def filename
69
+ "#{name}_\#{super}"
70
+ end
71
+ RUBY
72
+ new_outlet.class_eval(&block) if block
73
+ new_outlet
74
+ end
75
+
76
+ end
77
+
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,106 @@
1
+ require 'nozzle/adapter/base'
2
+
3
+ module Nozzle
4
+ module Adapter
5
+ # Installs default or custom adapter to a class.
6
+ #
7
+ # class Example
8
+ # attr_accessor :file, :avatar, :thumb
9
+ # include Nozzle::Adapter
10
+ # install_adapter( :file )
11
+ # install_adapter( :avatar, CustomAdapter )
12
+ # install_adapter( :thumb, Nozzle::Adapter::Image, :thumb_size => '90x60' )
13
+ # def save
14
+ # file_after_save; avatar_after_save; thumb_after_save
15
+ # end
16
+ # def destroy
17
+ # file_after_destroy; avatar_after_destroy; thumb_after_destroy
18
+ # end
19
+ # enc
20
+ #
21
+ # The class MUST have readers and writers to install corresponding adapters.
22
+ # +install_adapter+ overrides these methods and saves the originals in
23
+ # <tt>original_avatar</tt> and <tt>original_avatar=</tt> aliases.
24
+ # The originals are called to save and load the filename of stored asset.
25
+ #
26
+ # The class MUST call +avatar_after_save+ and +avatar_after_destroy+ after
27
+ # the corresponding events. +avatar_after_save+ does some IO to move
28
+ # or copy temporary file. +avatar_after_destroy+ deletes the stored file.
29
+ #
30
+ # Note: +options+ are only supported when +adapter+ is specified.
31
+ def install_adapter( column, adapter = nil, options = {} )
32
+ attr_accessor :"#{column}_adapter"
33
+ adapter_classes[column] = adapter || Base
34
+ adapter_options[column] = options
35
+
36
+ class_eval <<-RUBY, __FILE__, __LINE__+1
37
+ unless instance_methods.map(&:to_s).include?('original_#{column}')
38
+ alias_method :original_#{column}, :#{column}
39
+ alias_method :original_#{column}=, :#{column}=
40
+ end
41
+
42
+ def #{column}_adapter
43
+ @#{column}_adapter ||= self.class.adapter_classes[:#{column}].new(
44
+ self, :#{column}, nil, self.class.adapter_options[:#{column}] )
45
+ end
46
+
47
+ def #{column}
48
+ #{column}_adapter.load send( :original_#{column} )
49
+ end
50
+
51
+ def #{column}=(value)
52
+ send( :original_#{column}=, #{column}_adapter.dump(value) )
53
+ end
54
+
55
+ def #{column}_after_save
56
+ #{column}_adapter.store!
57
+ end
58
+
59
+ def #{column}_after_destroy
60
+ #{column}
61
+ #{column}_adapter.cleanup!
62
+ #{column}_adapter.unlink!
63
+ end
64
+ RUBY
65
+ end
66
+
67
+ def adapter_classes # :nodoc:
68
+ @adapter_classes ||= {}
69
+ end
70
+
71
+ def adapter_options # :nodoc:
72
+ @adapter_options ||= {}
73
+ end
74
+
75
+ ##
76
+ # :method: avatar
77
+ # Returns column adapter instance.
78
+ #
79
+ # Note: this method and the following 3 methods are dynamically created by
80
+ # +install_adapter+. These methods will be named according to the column
81
+ # name specified in first argument of +install_adapter+ call.
82
+ # This document explains methods created for column named <tt>:avatar</tt>.
83
+
84
+ ##
85
+ # :method: avatar=
86
+ #
87
+ # :call-seq: avatar=(value)
88
+ #
89
+ # Calls initialization routines to save file into class instance.
90
+ #
91
+ # The +value+ MUST be File, String, Hash or nil
92
+ # instance.avatar = File.open('tmp/031337.jpg')
93
+ # instance.avatar = 'tmp/031337.jpg'
94
+ # instance.avatar = { :filename => 'Cool file.jpg', :tempfile => cool }
95
+ # instance.avatar = nil
96
+ # If +value+ is nil then the stored file is deleted on +avatar_after_save+.
97
+
98
+ ##
99
+ # :method: avatar_after_save
100
+ # Calls Base#store! to move or copy temporary file to it's new store location.
101
+
102
+ ##
103
+ # :method: avatar_after_destroy
104
+ # Calls Base#unlink! to cleanup stored files.
105
+ end
106
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+
3
+ require 'nozzle'
4
+ require 'dm-core'
5
+
6
+ module Nozzle
7
+ module DataMapper
8
+ include Nozzle::Adapter
9
+
10
+ module Property
11
+ class Filename < ::DataMapper::Property::String
12
+ length 255
13
+ def custom?; true; end
14
+ end
15
+ end
16
+
17
+ def nozzle!( column, adapter = nil )
18
+ property column, Property::Filename unless properties.named?(column)
19
+ property :"#{column}_content_type", String, :length => 63 unless properties.named?("#{column}_content_type")
20
+ property :"#{column}_size", Integer unless properties.named?("#{column}_size")
21
+
22
+ install_adapter column, adapter
23
+
24
+ after :save, :"#{column}_after_save"
25
+ after :destroy, :"#{column}_after_destroy"
26
+ end
27
+ end
28
+ end
29
+
30
+ DataMapper::Model.append_extensions(Nozzle::DataMapper)
@@ -0,0 +1,3 @@
1
+ module Nozzle
2
+ VERSION = "0.1.1"
3
+ end
data/lib/nozzle.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'nozzle/version'
2
+ require 'nozzle/adapter'
data/nozzle.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ $LOAD_PATH << File.expand_path('../lib', __FILE__)
3
+ require 'nozzle/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'nozzle'
7
+ spec.version = Nozzle::VERSION
8
+ spec.description = 'Attachments for ruby rack'
9
+ spec.summary = 'A gem to store and serve attachments for ruby rack applications'
10
+
11
+ spec.authors = ['Igor Bochkariov']
12
+ spec.email = ['ujifgc@gmail.com']
13
+ spec.homepage = 'https://github.com/ujifgc/nozzle'
14
+ spec.license = 'MIT'
15
+
16
+ spec.require_paths = ['lib']
17
+ spec.files = `git ls-files`.split($/)
18
+ spec.test_files = spec.files.grep(%r{^test/})
19
+
20
+ spec.add_development_dependency 'bundler', '>= 1.3'
21
+ spec.add_development_dependency 'rake'
22
+ spec.add_development_dependency 'minitest'
23
+ end
Binary file
@@ -0,0 +1,4 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'nozzle'
3
+
4
+ require 'minitest/autorun'
@@ -0,0 +1,122 @@
1
+ require 'minitest_helper'
2
+ require 'fileutils'
3
+
4
+ describe Nozzle::Adapter::Base do
5
+ # these tests are ordered
6
+ class << self; define_method :test_order do :alpha end; end
7
+
8
+ before do
9
+ class Klass1
10
+ attr_accessor :avatar, :avatar_content_type, :avatar_size, :asset
11
+ def save
12
+ send :avatar_after_save
13
+ end
14
+ def destroy
15
+ send :avatar_after_destroy
16
+ end
17
+ end
18
+ Klass1.send :extend, Nozzle::Adapter
19
+ Klass1.install_adapter :avatar
20
+ Klass1.install_adapter :asset
21
+ end
22
+
23
+ it 'should raise ENOENT on non-existing asset' do
24
+ inst = Klass1.new
25
+ lambda do
26
+ inst.avatar = 'test/fixtures/non-existing.jpg'
27
+ end.must_raise Errno::ENOENT
28
+ end
29
+
30
+ it 'should raise ArgumentError on bad arguments' do
31
+ inst = Klass1.new
32
+ lambda do
33
+ inst.avatar = ['array']
34
+ end.must_raise ArgumentError
35
+ lambda do
36
+ inst.avatar = :symbol
37
+ end.must_raise ArgumentError
38
+ end
39
+
40
+ it 'should save file into public folder and destroy it' do
41
+ public_path = 'public/uploads/Klass1/avatar/test-697x960.jpg'
42
+
43
+ inst = Klass1.new
44
+ inst.avatar = 'test/fixtures/test-697x960.jpg'
45
+ inst.avatar.filename.must_equal 'test-697x960.jpg'
46
+
47
+ inst.save
48
+ inst.avatar.path.must_equal public_path
49
+ File.exists?(public_path).must_equal true
50
+
51
+ inst.save
52
+ inst.avatar.path.must_equal public_path
53
+ File.exists?(public_path).must_equal true
54
+
55
+ inst.destroy
56
+ File.exists?(public_path).must_equal false
57
+ end
58
+
59
+ it 'should detect file properties if the properties are available' do
60
+ inst = Klass1.new
61
+ inst.avatar = 'test/fixtures/test-697x960.jpg'
62
+ inst.avatar.content_type.must_equal 'image/jpeg'
63
+ inst.avatar.size.must_equal File.size('test/fixtures/test-697x960.jpg')
64
+
65
+ inst.asset = 'test/fixtures/test-697x960.jpg'
66
+ inst.asset.content_type.must_equal ''
67
+ inst.asset.size.must_equal -1
68
+ end
69
+
70
+ it 'should return intermediate tempfile path or stored path' do
71
+ inst = Klass1.new
72
+ inst.avatar = 'test/fixtures/test-697x960.jpg'
73
+ inst.avatar.access_path.must_equal File.expand_path('test/fixtures/test-697x960.jpg')
74
+ inst.save
75
+ inst.avatar.access_path.must_equal 'public/uploads/Klass1/avatar/test-697x960.jpg'
76
+ end
77
+
78
+ it 'should respect custom filename' do
79
+ public_path = 'public/uploads/Klass1/avatar/girl-and-square.jpg'
80
+
81
+ inst = Klass1.new
82
+ inst.avatar = { :tempfile => 'test/fixtures/test-697x960.jpg', :filename => 'girl-and-square.jpg' }
83
+ inst.avatar.filename.must_equal 'girl-and-square.jpg'
84
+
85
+ inst.save
86
+ inst.avatar.path.must_equal public_path
87
+ File.exists?(public_path).must_equal true
88
+
89
+ inst.destroy
90
+ File.exists?(public_path).must_equal false
91
+ end
92
+
93
+ it 'should accept string-keyed hash' do
94
+ public_path = 'public/uploads/Klass1/avatar/girl-and-square.jpg'
95
+
96
+ inst = Klass1.new
97
+ inst.avatar = { 'tempfile' => 'test/fixtures/test-697x960.jpg', 'filename' => 'girl-and-square.jpg' }
98
+ inst.avatar.filename.must_equal 'girl-and-square.jpg'
99
+
100
+ inst.save
101
+ inst.avatar.path.must_equal public_path
102
+ File.exists?(public_path).must_equal true
103
+
104
+ inst.destroy
105
+ File.exists?(public_path).must_equal false
106
+ end
107
+
108
+ it 'should destroy files whit was saved before' do
109
+ public_path = 'public/uploads/Klass1/avatar/test-697x960.jpg'
110
+ inst = Klass1.new
111
+ FileUtils.mkdir_p 'public/uploads/Klass1/avatar'
112
+ FileUtils.cp 'test/fixtures/test-697x960.jpg', public_path
113
+ inst.instance_eval('@avatar = "test-697x960.jpg"')
114
+ inst.destroy
115
+ File.exists?(public_path).must_equal false
116
+ end
117
+
118
+ after do
119
+ FileUtils.rm_rf 'public'
120
+ end
121
+
122
+ end
@@ -0,0 +1,48 @@
1
+ require 'minitest_helper'
2
+ require 'fileutils'
3
+ require 'nozzle/adapter/image'
4
+
5
+ describe Nozzle::Adapter::Image do
6
+ # these tests are ordered
7
+ class << self; define_method :test_order do :alpha end; end
8
+
9
+ before do
10
+ class Klass3
11
+ def avatar1; @avatar1; end
12
+ def avatar1=(value); @avatar1 = value; end
13
+ def avatar2; @avatar2; end
14
+ def avatar2=(value); @avatar2 = value; end
15
+ def save
16
+ send :avatar1_after_save
17
+ send :avatar2_after_save
18
+ end
19
+ def destroy
20
+ send :avatar1_after_destroy
21
+ send :avatar2_after_destroy
22
+ end
23
+ end
24
+ Klass3.send :extend, Nozzle::Adapter
25
+ Klass3.install_adapter :avatar1, Nozzle::Adapter::Image
26
+ Klass3.install_adapter :avatar2, Nozzle::Adapter::Image, :thumb_size => '90x60'
27
+ end
28
+
29
+ it 'should have default url' do
30
+ inst = Klass3.new
31
+ inst.avatar1.url.must_equal '/images/image_missing.png'
32
+ inst.avatar2.settings[:thumb_size].must_equal '90x60'
33
+ inst.avatar1.settings[:thumb_size].must_equal '200x150'
34
+
35
+ inst.avatar1 = 'test/fixtures/test-697x960.jpg'
36
+ inst.avatar2 = 'test/fixtures/test-697x960.jpg'
37
+ inst.save
38
+ inst.avatar1.thumb.prepare!
39
+ `identify #{inst.avatar1.thumb.path}`.must_match /JPEG 109x150/
40
+ inst.avatar2.thumb.prepare!
41
+ `identify #{inst.avatar2.thumb.path}`.must_match /JPEG 44x60/
42
+ end
43
+
44
+ after do
45
+ FileUtils.rm_rf 'public'
46
+ end
47
+
48
+ end
@@ -0,0 +1,104 @@
1
+ require 'minitest_helper'
2
+ require 'fileutils'
3
+
4
+ describe Nozzle::Adapter::Outlet do
5
+ # these tests are ordered
6
+ class << self; define_method :test_order do :alpha end; end
7
+
8
+ before do
9
+ class Klass2
10
+ def avatar
11
+ @avatar
12
+ end
13
+ def avatar=(value)
14
+ @avatar = value
15
+ end
16
+ def save
17
+ send :avatar_after_save
18
+ end
19
+ def destroy
20
+ send :avatar_after_destroy
21
+ end
22
+ end
23
+ Klass2.send :extend, Nozzle::Adapter
24
+ class BaseOutlet < Nozzle::Adapter::Base
25
+ def filename
26
+ "#{Time.now.strftime('%Y%m%d')}_#{super}"
27
+ end
28
+
29
+ outlet :thumb do
30
+ def filename
31
+ 'ava_' + super
32
+ end
33
+ def prepare( original, result )
34
+ `convert #{original} -thumbnail x96 #{result}`
35
+ end
36
+ outlet :mini
37
+ end
38
+ outlet :big do
39
+ def relative_folder
40
+ File.join 'big', @model.to_s
41
+ end
42
+ end
43
+ end
44
+ Klass2.install_adapter :avatar, BaseOutlet
45
+ end
46
+
47
+ it 'should install an adapter with custom filename and outlets' do
48
+ date = Time.now.strftime('%Y%m%d')
49
+ inst = Klass2.new
50
+ inst.avatar = 'test/fixtures/test-697x960.jpg'
51
+ inst.save
52
+ inst.avatar.filename.must_equal "#{date}_test-697x960.jpg"
53
+ inst.avatar.thumb.filename.must_equal "ava_#{date}_test-697x960.jpg"
54
+ inst.avatar.big.path.must_equal "public/uploads/big/Klass2/big_#{date}_test-697x960.jpg"
55
+ inst.avatar.thumb.mini.path.must_equal "public/uploads/Klass2/avatar/mini_ava_#{date}_test-697x960.jpg"
56
+ inst.avatar.respond_to?(:version_name).must_equal false, 'original adapter must not have version name'
57
+ inst.avatar.thumb.version_name.must_equal 'thumb'
58
+ inst.avatar.thumb.mini.version_name.must_equal 'thumb_mini'
59
+
60
+ inst.avatar.thumb.prepare!
61
+ `identify #{inst.avatar.thumb.path}`.must_match /JPEG 70x96/
62
+ inst.avatar.big.prepare!
63
+ `identify #{inst.avatar.big.path}`.must_match /JPEG 697x960/
64
+
65
+ inst.avatar.cleanup!
66
+ File.exists?(inst.avatar.path).must_equal true, 'outlet must not cleanup the original'
67
+ File.exists?(inst.avatar.thumb.path).must_equal false, 'outlet must cleanup its cache'
68
+ File.exists?(inst.avatar.big.path).must_equal false
69
+ File.exists?(inst.avatar.big.system_path).must_equal false, 'outlet must cleanup its folders'
70
+
71
+ inst.avatar.big.prepare!
72
+ `identify #{inst.avatar.big.path}`.must_match /JPEG 697x960/
73
+ a,b,a1,b1 = [ inst.avatar.path, inst.avatar.big.path, inst.avatar.system_path, inst.avatar.big.system_path ]
74
+ inst.destroy
75
+ File.exists?(a).must_equal false
76
+ File.exists?(b).must_equal false
77
+ File.exists?(a1).must_equal false
78
+ File.exists?(b1).must_equal false
79
+ end
80
+
81
+ it 'should move temporary file' do
82
+ temporary_path = '/tmp/test-697x960.jpg'
83
+ FileUtils.cp 'test/fixtures/test-697x960.jpg', temporary_path
84
+ inst1 = Klass2.new
85
+ inst1.avatar = temporary_path
86
+ inst1.save
87
+ File.exists?(inst1.avatar.path).must_equal true
88
+ File.exists?(temporary_path).must_equal false
89
+ end
90
+
91
+ it 'should copy non-temporary file' do
92
+ permanent_path = 'test/fixtures/test-697x960.jpg'
93
+ inst1 = Klass2.new
94
+ inst1.avatar = permanent_path
95
+ inst1.save
96
+ File.exists?(inst1.avatar.path).must_equal true
97
+ File.exists?(permanent_path).must_equal true
98
+ end
99
+
100
+ after do
101
+ FileUtils.rm_rf 'public'
102
+ end
103
+
104
+ end
@@ -0,0 +1,40 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Nozzle::Adapter do
4
+
5
+ before do
6
+ class Klass0
7
+ def avatar
8
+ @avatar
9
+ end
10
+ def avatar=(value)
11
+ @avatar = value
12
+ end
13
+ end
14
+ Klass0.send :extend, Nozzle::Adapter
15
+ Klass0.install_adapter :avatar
16
+ end
17
+
18
+ it 'should be able to install itself' do
19
+ Klass0.adapter_classes.must_equal :avatar => Nozzle::Adapter::Base
20
+ end
21
+
22
+ it 'should register instance methods' do
23
+ @inst = Klass0.new
24
+ @inst.respond_to?(:avatar_adapter).must_equal true
25
+ @inst.respond_to?(:avatar).must_equal true
26
+ @inst.respond_to?(:avatar=).must_equal true
27
+ @inst.respond_to?(:avatar_after_save).must_equal true
28
+ @inst.respond_to?(:avatar_after_destroy).must_equal true
29
+ @inst.avatar_adapter.class.must_equal Nozzle::Adapter::Base
30
+ end
31
+
32
+ it 'should register instance methods one and only one time' do
33
+ Klass0.install_adapter :avatar
34
+ Klass0.install_adapter :avatar
35
+ @inst = Klass0.new
36
+ @inst.avatar.class.must_equal Nozzle::Adapter::Base
37
+ @inst.original_avatar.class.wont_equal Nozzle::Adapter::Base
38
+ end
39
+
40
+ end
@@ -0,0 +1,7 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Nozzle do
4
+ it 'should have a version' do
5
+ Nozzle::VERSION.must_match /.+/
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nozzle
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Igor Bochkariov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-06-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Attachments for ruby rack
56
+ email:
57
+ - ujifgc@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - .gitignore
63
+ - .travis.yml
64
+ - Gemfile
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - lib/nozzle.rb
69
+ - lib/nozzle/adapter.rb
70
+ - lib/nozzle/adapter/base.rb
71
+ - lib/nozzle/adapter/image.rb
72
+ - lib/nozzle/adapter/outlet.rb
73
+ - lib/nozzle/datamapper.rb
74
+ - lib/nozzle/version.rb
75
+ - nozzle.gemspec
76
+ - test/fixtures/test-697x960.jpg
77
+ - test/minitest_helper.rb
78
+ - test/nozzle_adapter_base_test.rb
79
+ - test/nozzle_adapter_image_test.rb
80
+ - test/nozzle_adapter_outlet_test.rb
81
+ - test/nozzle_adapter_test.rb
82
+ - test/nozzle_test.rb
83
+ homepage: https://github.com/ujifgc/nozzle
84
+ licenses:
85
+ - MIT
86
+ metadata: {}
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - '>='
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ requirements: []
102
+ rubyforge_project:
103
+ rubygems_version: 2.0.3
104
+ signing_key:
105
+ specification_version: 4
106
+ summary: A gem to store and serve attachments for ruby rack applications
107
+ test_files:
108
+ - test/fixtures/test-697x960.jpg
109
+ - test/minitest_helper.rb
110
+ - test/nozzle_adapter_base_test.rb
111
+ - test/nozzle_adapter_image_test.rb
112
+ - test/nozzle_adapter_outlet_test.rb
113
+ - test/nozzle_adapter_test.rb
114
+ - test/nozzle_test.rb