nozzle 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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