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 +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +32 -0
- data/Rakefile +11 -0
- data/lib/nozzle/adapter/base.rb +246 -0
- data/lib/nozzle/adapter/image.rb +27 -0
- data/lib/nozzle/adapter/outlet.rb +80 -0
- data/lib/nozzle/adapter.rb +106 -0
- data/lib/nozzle/datamapper.rb +30 -0
- data/lib/nozzle/version.rb +3 -0
- data/lib/nozzle.rb +2 -0
- data/nozzle.gemspec +23 -0
- data/test/fixtures/test-697x960.jpg +0 -0
- data/test/minitest_helper.rb +4 -0
- data/test/nozzle_adapter_base_test.rb +122 -0
- data/test/nozzle_adapter_image_test.rb +48 -0
- data/test/nozzle_adapter_outlet_test.rb +104 -0
- data/test/nozzle_adapter_test.rb +40 -0
- data/test/nozzle_test.rb +7 -0
- metadata +114 -0
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
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
|
+
[](https://travis-ci.org/ujifgc/nozzle)
|
2
|
+
[](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,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)
|
data/lib/nozzle.rb
ADDED
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,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
|
data/test/nozzle_test.rb
ADDED
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
|