stash-magic 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/MIT_LICENCE +19 -0
- data/README.rdoc +213 -0
- data/spec.rb +237 -0
- data/stash_magic.gemspec +13 -0
- data/stash_magic.rb +191 -0
- metadata +72 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
pkg/*
|
data/MIT_LICENCE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2010 Mickael Riga
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,213 @@
|
|
1
|
+
= Stash Magic (BETA)
|
2
|
+
|
3
|
+
Stash Magic provides a very simple interface for dealing with file system attachments in a database and help you with thumbnails or other styles via ImageMagick (hence, the name). Features are:
|
4
|
+
|
5
|
+
- Many attachments per database entry
|
6
|
+
- ImageMagick string builder
|
7
|
+
- after_stash hook for creating thumbnails or other styles automatically when attachment is created or updated.
|
8
|
+
- Specs for Sequel ORM but pretty easy to adapt
|
9
|
+
- Easy to understand (one file) module
|
10
|
+
|
11
|
+
This is still in Beta version built with simplicity in mind.
|
12
|
+
Don't hesitate to contact me for any improvement, suggestion, or bug fixing.
|
13
|
+
|
14
|
+
I've made the design choice not to build a Sequel plugin because I'd like StashMagic to work with other ORMs in the future.
|
15
|
+
So any test or help is welcome.
|
16
|
+
|
17
|
+
= How to use
|
18
|
+
|
19
|
+
First you have to require the module:
|
20
|
+
|
21
|
+
require 'stash_magic'
|
22
|
+
|
23
|
+
And then inside your model class, you have to include the module and declare where your public directory is:
|
24
|
+
|
25
|
+
class Treasure < ::Sequel::Model
|
26
|
+
include ::StashMagic
|
27
|
+
self.public_root = ::File.expand_path(::File.dirname(__FILE__)+'/public')
|
28
|
+
end
|
29
|
+
|
30
|
+
The module has a method to do both in one line though:
|
31
|
+
|
32
|
+
class Treasure < ::Sequel::Model
|
33
|
+
::StashMagic.with_public_root ::File.expand_path(::File.dirname(__FILE__)+'/public')
|
34
|
+
end
|
35
|
+
|
36
|
+
After that, for each attachment you want, you need to have a column in the database as a string. And then you declare them with method Model#stash:
|
37
|
+
|
38
|
+
class Treasure < ::Sequel::Model
|
39
|
+
::StashMagic.with_public_root ::File.expand_path(::File.dirname(__FILE__)+'/public')
|
40
|
+
|
41
|
+
stash :map
|
42
|
+
stash :stamp
|
43
|
+
end
|
44
|
+
|
45
|
+
This method accepts an optional hash as a second argument which is not used by the module itself, but could be handy for you as you can have it in the stash reflection:
|
46
|
+
|
47
|
+
class Treasure < ::Sequel::Model
|
48
|
+
::StashMagic.with_public_root ::File.expand_path(::File.dirname(__FILE__)+'/public')
|
49
|
+
|
50
|
+
stash :map
|
51
|
+
stash :stamp, :accept_gif => false, :limit => 512000
|
52
|
+
end
|
53
|
+
|
54
|
+
The method Treasure.stash_reflection would return:
|
55
|
+
|
56
|
+
{
|
57
|
+
:map => {},
|
58
|
+
:stamp => {:accept_gif => false, :limit => 512000}
|
59
|
+
}
|
60
|
+
|
61
|
+
When building your html forms, just make sure that your stash inputs are of the type 'file', and StashMagic will deal with everything else. The getters will return a hash with the following values:
|
62
|
+
|
63
|
+
@treasure_instance.map # { :name => 'map.pdf', :type => 'application/pdf', :size => 1024 }
|
64
|
+
@treasure_instance.stamp # nil if there is no stamp yet
|
65
|
+
|
66
|
+
Please note that the file name will always be the name of the attachment with the extention of the file you've uploaded (pdf, jpg ...)
|
67
|
+
This makes StashMagic internals a lot easier for dealing with styles (ex: thumbnails) as we'll see later.
|
68
|
+
|
69
|
+
You can also use the setters to delete an attachment:
|
70
|
+
|
71
|
+
@treasure_instance.map = nil # Will delete this attachment as expected
|
72
|
+
|
73
|
+
When you want to use attachment in your application, you can retrieve the file url like that:
|
74
|
+
|
75
|
+
@treasure_instance.file_url(:map) # The original file
|
76
|
+
@treasure_instance.file_url(:map, 'thumb.gif') # The picture in a thumb.gif style (see next chapter to learn about styles)
|
77
|
+
|
78
|
+
You might also want to do things on the server side like changing rights on the image or whatever. For that purpose, there is a third argument which is a boolean. When set to true, it will give you the absolute path to the file:
|
79
|
+
|
80
|
+
@treasure_instance.file_url(:map, nil, true) # /absolute/path/to/public/stash/Treasure/1/map.pdf
|
81
|
+
@treasure_instance.file_url(:map, 'thumb.gif', true) # /absolute/path/to/public/stash/Treasure/1/map.thumb.gif
|
82
|
+
|
83
|
+
= Thumbnails and Other Styles
|
84
|
+
|
85
|
+
One of the main requirements of StashMagic was to provide a way to deal quite easily with styles, and to deal with them whenever you want to, not only automaticaly when you save an attachment. The reason for that last point is because I was working at the same time on a cropping tool and realized That I needed to be able to create styles whenever I wanted without changing the way my attachment manager works.
|
86
|
+
|
87
|
+
The simpliest solution I came up with was to be quite strict with names. So far, when StashMagic asks for a style, what it needs is a suffix which contains the extention you want the style to be saved to.
|
88
|
+
|
89
|
+
Say for example you have an attachment called :portrait and you want a version called "mini" which is gonna be a gif. Your style should be called:
|
90
|
+
|
91
|
+
mini.gif
|
92
|
+
|
93
|
+
I just find it makes sense and saves one argument on some methods that are already verbose.
|
94
|
+
|
95
|
+
Now if you really want to create styles, you need to have ImageMagick installed. ImageMagick is a very good and complete graphic library. You'll find more on the link below, but for the time being, just think of it as a Photoshop in command line:
|
96
|
+
|
97
|
+
http://www.imagemagick.org
|
98
|
+
|
99
|
+
Even though StashMagic provides a builder for ImageMagick scripts, I suggest you learn a little bit about them for the following reasons:
|
100
|
+
|
101
|
+
- This is not much harder than learning to make things with methods and arguments
|
102
|
+
- It makes you able to use it on it's own
|
103
|
+
- The builder is limited, not as complete as real ImageMagick ruby wrapper like RMagick
|
104
|
+
- I like to believe it's fun as well (not only powerful)
|
105
|
+
|
106
|
+
So for the couragous amongst you, here is the way you create a very simple style for the portrait attachment:
|
107
|
+
|
108
|
+
@treasure_instance.convert :portrait, '-resize 100x75', 'mini.gif'
|
109
|
+
|
110
|
+
The middle argument is the piece of script used in the main ImageMagick command called: convert
|
111
|
+
It is everything that happens between the source and the destination (hence its position in the list of arguments).
|
112
|
+
|
113
|
+
This will create your mini version of the portrait. The url for this image will be:
|
114
|
+
|
115
|
+
@treasure_instance.file_url(:portrait, 'mini.gif')
|
116
|
+
|
117
|
+
If you master ImageMagick, you can really do a lot with that. Nevertheless here is what you can use, as I have to admit that some things like geometry are not easy to get the first time. Here is the so-called string builder:
|
118
|
+
|
119
|
+
@treasure_instance.image_magick :portrait, 'mini.gif' do
|
120
|
+
im_resize(100, 75)
|
121
|
+
end
|
122
|
+
|
123
|
+
Not really much easier huh ?!?
|
124
|
+
|
125
|
+
Ok so in the builder, you can use some pre-defined operations (prefixed with 'im_' standing for ImageMagick) that will occur in the order you write them. It is quite limited for the moment, but I will complete the list in time. Here is that list:
|
126
|
+
|
127
|
+
== im_write( string )
|
128
|
+
|
129
|
+
This is the most simple one. It is for when you know how to write a piece of the script. For example you could use:
|
130
|
+
|
131
|
+
im_write("-negate")
|
132
|
+
|
133
|
+
It will negate the image at this stage.
|
134
|
+
|
135
|
+
== im_crop( width, height, x, y )
|
136
|
+
|
137
|
+
Self explainatory as well, it will create a crop using the values provided.
|
138
|
+
|
139
|
+
== im_resize( width, height, geometry_option=nil, gravity=nil )
|
140
|
+
|
141
|
+
This one is a little bit more complicated than it sounds. You can play with options a lot.
|
142
|
+
|
143
|
+
First thing to try is to give only width and height but with one of them nil. This will resize only by width or height, but keeping the original ratio.
|
144
|
+
|
145
|
+
If you do the same but with both values, you have to make sure that the ratio is the same as the original. Otherwise the resulting image will be streched to fit in the proportions you provided.
|
146
|
+
|
147
|
+
To solve the above problem, you can use the geometry_option. For example, the geometry option: '^' will more or less crop your image so that it keeps its original ratio while fitting perfectly the proportions you provided. In the future, I will find symbols to use instead of their cryptic names I guess. This is the most useful one (while not available on old versions of ImageMagick < 6.3.8-2). You can also use '>' and '<' which will only proceed if the original image is bigger (or smaller for '<') than the proportions you provided.
|
148
|
+
|
149
|
+
For more info on geometry, read this:
|
150
|
+
|
151
|
+
http://www.imagemagick.org/script/command-line-processing.php#geometry
|
152
|
+
|
153
|
+
The last argument is useful when you use '^' as a third argument for example. If the image as to be cropped, we need to know how to crop it. This is the gravity. By default the gravity is 'center', but you might want to use 'north', 'south' ...
|
154
|
+
|
155
|
+
More about gravity here:
|
156
|
+
|
157
|
+
http://www.imagemagick.org/script/command-line-options.php#gravity
|
158
|
+
|
159
|
+
== im_negate
|
160
|
+
|
161
|
+
Simply negate the image
|
162
|
+
|
163
|
+
= More about the builder
|
164
|
+
|
165
|
+
Here a more complete example for the builder:
|
166
|
+
|
167
|
+
@treasure_instance.image_magick :portrait, 'mini.gif' do
|
168
|
+
im_negate
|
169
|
+
im_crop(200,100,20,10)
|
170
|
+
im_resize(200, 100, '^', 'North')
|
171
|
+
end
|
172
|
+
|
173
|
+
Which will secretly do something like:
|
174
|
+
|
175
|
+
convert /path/to/portrait.jpg -negate -crop 200x100+20+10 +repage -resize '200x100^' -gravity North -extent 200x100 /path/to/portrait.mini.gif
|
176
|
+
|
177
|
+
= How to create thumbnails on the flight (The Hook)
|
178
|
+
|
179
|
+
It is of course possible. StashMagic provides a hook called after_stash which takes the attachment_name as an argument. This hook is implemented by default and create automaticaly for every image a thumbnail called 'stash_thumb.gif'.
|
180
|
+
|
181
|
+
What you have to do is overwrite the hook. For example, say you want every attachment to have a 200x200 perfect squared version:
|
182
|
+
|
183
|
+
after_stash(attachment_name)
|
184
|
+
image_magick(attachment_name, 'square.jpg') { im_resize(200, 200, '^') }
|
185
|
+
end
|
186
|
+
|
187
|
+
Of course you can do something different for any attachment. You just need to use the attachment name in a case statement for example. Or you can do something different depending on the type of file using the getters. For example:
|
188
|
+
|
189
|
+
after_stash(attachment_name)
|
190
|
+
attachment_hash = self.send(attachment_name)
|
191
|
+
image_magick(attachment_name, 'square.jpg') { im_resize(200, 200, '^') } if attachment_hash[:type][/^image\//]
|
192
|
+
end
|
193
|
+
|
194
|
+
Will do the same but only if the mime type of the file starts with 'image/' (which means it's an image).
|
195
|
+
|
196
|
+
|
197
|
+
= More Details
|
198
|
+
|
199
|
+
For more details, you can have a look at the specs to see how it's used or contact me on github if you have any question: http://github.com/mig-hub
|
200
|
+
|
201
|
+
The project is speced with
|
202
|
+
- Bacon 1.1.0
|
203
|
+
- Sequel 3.14.0
|
204
|
+
- ImageMagick 6.5.8
|
205
|
+
|
206
|
+
= Change Log
|
207
|
+
|
208
|
+
- 0.0.1 Begins
|
209
|
+
- 0.0.2 Add im_negate to the ImageMagick builder
|
210
|
+
|
211
|
+
== Copyright
|
212
|
+
|
213
|
+
(c) 2010 Mickael Riga - see MIT_LICENCE for details
|
data/spec.rb
ADDED
@@ -0,0 +1,237 @@
|
|
1
|
+
# =========
|
2
|
+
# = Setup =
|
3
|
+
# =========
|
4
|
+
|
5
|
+
F = ::File
|
6
|
+
D = ::Dir
|
7
|
+
|
8
|
+
require 'rubygems'
|
9
|
+
require 'bacon'
|
10
|
+
Bacon.summary_on_exit
|
11
|
+
|
12
|
+
require 'sequel'
|
13
|
+
DB = Sequel.sqlite
|
14
|
+
|
15
|
+
require 'tempfile'
|
16
|
+
|
17
|
+
require F.dirname(__FILE__)+'/stash_magic'
|
18
|
+
|
19
|
+
class Treasure < ::Sequel::Model
|
20
|
+
PUBLIC = F.expand_path(F.dirname(__FILE__)+'/public')
|
21
|
+
::StashMagic.with_public_root(PUBLIC)
|
22
|
+
|
23
|
+
plugin :schema
|
24
|
+
set_schema do
|
25
|
+
primary_key :id
|
26
|
+
Integer :age
|
27
|
+
String :map # jpeg
|
28
|
+
String :mappy # jpeg - Used to see if mappy files are not destroyed when map is (because it starts the same)
|
29
|
+
String :instructions #pdf
|
30
|
+
end
|
31
|
+
create_table unless table_exists?
|
32
|
+
|
33
|
+
stash :map
|
34
|
+
stash :mappy
|
35
|
+
stash :instructions
|
36
|
+
|
37
|
+
def validate
|
38
|
+
errors[:age] << "Not old enough" unless (self.age.nil? || self.age>10)
|
39
|
+
errors[:instructions] << "Too big" if (!self.instructions.nil? && self.instructions[:size].to_i>46000)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class BadTreasure < ::Sequel::Model
|
44
|
+
include ::StashMagic
|
45
|
+
|
46
|
+
plugin :schema
|
47
|
+
set_schema do
|
48
|
+
primary_key :id
|
49
|
+
String :map # jpeg
|
50
|
+
String :instructions #pdf
|
51
|
+
end
|
52
|
+
create_table unless table_exists?
|
53
|
+
end
|
54
|
+
|
55
|
+
# Make temporary public folder
|
56
|
+
D.mkdir(Treasure::PUBLIC) unless F.exists?(Treasure::PUBLIC)
|
57
|
+
|
58
|
+
# =========
|
59
|
+
# = Tests =
|
60
|
+
# =========
|
61
|
+
|
62
|
+
describe ::StashMagic do
|
63
|
+
|
64
|
+
`convert rose: #{Treasure::PUBLIC}/rose.jpg` unless F.exists?(Treasure::PUBLIC+'/rose.jpg') # Use ImageMagick to build a tmp image to use
|
65
|
+
`convert granite: #{Treasure::PUBLIC}/granite.gif` unless F.exists?(Treasure::PUBLIC+'/granite.gif') # Use ImageMagick to build a tmp image to use
|
66
|
+
`convert rose: #{Treasure::PUBLIC}/rose.pdf` unless F.exists?(Treasure::PUBLIC+'/rose.pdf') # Use ImageMagick to build a tmp image to use
|
67
|
+
`convert logo: #{Treasure::PUBLIC}/logo.pdf` unless F.exists?(Treasure::PUBLIC+'/logo.pdf')
|
68
|
+
|
69
|
+
def mock_upload(uploaded_file_path, content_type, binary=false)
|
70
|
+
n = F.basename(uploaded_file_path)
|
71
|
+
f = ::Tempfile.new(n)
|
72
|
+
f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
|
73
|
+
f.binmode if binary
|
74
|
+
::FileUtils.copy_file(uploaded_file_path, f.path)
|
75
|
+
{
|
76
|
+
:filename => n,
|
77
|
+
:type => content_type,
|
78
|
+
:tempfile => f
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
before do
|
83
|
+
@img = mock_upload(Treasure::PUBLIC+'/rose.jpg', 'image/jpeg', true)
|
84
|
+
@img2 = mock_upload(Treasure::PUBLIC+'/granite.gif', 'image/gif', true)
|
85
|
+
@pdf = mock_upload(Treasure::PUBLIC+'/rose.pdf', 'application/pdf', true)
|
86
|
+
@pdf2 = mock_upload(Treasure::PUBLIC+'/logo.pdf', 'application/pdf', true)
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'Should Include via Stash::with_public_root' do
|
90
|
+
Treasure.public_root.should==Treasure::PUBLIC
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'Should create stash and model folder when included' do
|
94
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure').should==true
|
95
|
+
end
|
96
|
+
|
97
|
+
it "Should stash entries with Class::stash and have reflection" do
|
98
|
+
Treasure.stash_reflection.keys.include?(:map).should==true
|
99
|
+
Treasure.stash_reflection.keys.include?(:instructions).should==true
|
100
|
+
end
|
101
|
+
|
102
|
+
it "Should give instance its own file_path" do
|
103
|
+
# Normal path
|
104
|
+
@t = Treasure.create
|
105
|
+
@t.file_path.should=="/stash/Treasure/#{@t.id}"
|
106
|
+
# Anonymous path
|
107
|
+
Treasure.new.file_path.should=='/stash/Treasure/tmp'
|
108
|
+
# Normal path full
|
109
|
+
@t = Treasure.create
|
110
|
+
@t.file_path(true).should==Treasure::PUBLIC+"/stash/Treasure/#{@t.id}"
|
111
|
+
# Anonymous path full
|
112
|
+
Treasure.new.file_path(true).should==Treasure::PUBLIC+'/stash/Treasure/tmp'
|
113
|
+
end
|
114
|
+
|
115
|
+
it "Should always raise on file_path if public_root is not declared" do
|
116
|
+
lambda { BadTreasure.new.file_path }.should.raise(RuntimeError).message.should=='BadTreasure.public_root is not declared'
|
117
|
+
end
|
118
|
+
|
119
|
+
it "Should not raise on setters eval when value already nil" do
|
120
|
+
Treasure.new.map.should==nil
|
121
|
+
end
|
122
|
+
|
123
|
+
it "Should have correct file_url values" do
|
124
|
+
# Original with no file - so we are not sure about extention
|
125
|
+
Treasure.new.file_url(:map).should==nil
|
126
|
+
# Original with file but not saved
|
127
|
+
Treasure.new(:map=>@img).file_url(:map).should=='/stash/Treasure/tmp/map.jpg'
|
128
|
+
# Style with file but not saved
|
129
|
+
Treasure.new(:map=>@img).file_url(:map, 'thumb.jpg').should=='/stash/Treasure/tmp/map.thumb.jpg' #not the right extention
|
130
|
+
end
|
131
|
+
|
132
|
+
it "Should save the attachments when creating entry" do
|
133
|
+
@t = Treasure.create(:map => @img, :instructions => @pdf)
|
134
|
+
@t.map.should=={:name=>'map.jpg',:type=>'image/jpeg',:size=>2074}
|
135
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/map.jpg').should==true
|
136
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/instructions.pdf').should==true
|
137
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/map.stash_thumb.gif').should==true
|
138
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/instructions.stash_thumb.gif').should==false
|
139
|
+
end
|
140
|
+
|
141
|
+
it "Should update attachment when updating entry" do
|
142
|
+
@t = Treasure.create(:map => @img).update(:map=>@img2)
|
143
|
+
@t.map.should=={:name=>'map.gif',:type=>'image/gif',:size=>7037}
|
144
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/map.gif').should==true
|
145
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/map.stash_thumb.gif').should==true
|
146
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/map.jpg').should==false
|
147
|
+
end
|
148
|
+
|
149
|
+
it "Should destroy its folder when destroying entry" do
|
150
|
+
@t = Treasure.create(:map => @img)
|
151
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s).should==true
|
152
|
+
@t.destroy
|
153
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s).should==false
|
154
|
+
end
|
155
|
+
|
156
|
+
it "Should be able to remove attachments when column is set to nil" do
|
157
|
+
@t = Treasure.create(:map => @img, :mappy => @img2)
|
158
|
+
@t.map.should=={:name=>'map.jpg',:type=>'image/jpeg',:size=>2074}
|
159
|
+
@t.mappy.should=={:name=>'mappy.gif',:type=>'image/gif',:size=>7037}
|
160
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/map.jpg').should==true
|
161
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/mappy.gif').should==true
|
162
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/map.stash_thumb.gif').should==true
|
163
|
+
@t.update(:map=>nil)
|
164
|
+
@t.map.should==nil
|
165
|
+
@t.mappy.should=={:name=>'mappy.gif',:type=>'image/gif',:size=>7037}
|
166
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/map.jpg').should==false
|
167
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/mappy.gif').should==true
|
168
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/map.stash_thumb.gif').should==false
|
169
|
+
end
|
170
|
+
|
171
|
+
it "Should be able to build image tags" do
|
172
|
+
@t = Treasure.create(:map => @img)
|
173
|
+
tag = @t.build_image_tag(:map,nil,:alt => 'Amazing Map')
|
174
|
+
tag.should.match(/^<img\s.+\s\/>$/)
|
175
|
+
tag.should.match(/\ssrc="\/stash\/Treasure\/#{@t.id}\/map.jpg"\s/)
|
176
|
+
tag.should.match(/\salt="Amazing Map"\s/)
|
177
|
+
tag.should.match(/\stitle=""\s/)
|
178
|
+
end
|
179
|
+
|
180
|
+
it "Should be able to handle validations" do
|
181
|
+
@t = Treasure.new(:instructions => @pdf2)
|
182
|
+
@t.valid?.should==false
|
183
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/instructions.pdf').should==false
|
184
|
+
@t.set(:instructions => @pdf, :age => 8)
|
185
|
+
@t.valid?.should==false
|
186
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/instructions.pdf').should==false
|
187
|
+
@t.set(:age => 12)
|
188
|
+
@t.valid?.should==true
|
189
|
+
@t.save
|
190
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/instructions.pdf').should==true
|
191
|
+
end
|
192
|
+
|
193
|
+
it "Should not raise when updating the entry with blank string - which means the attachment is untouched" do
|
194
|
+
@t = Treasure.create(:instructions => @pdf)
|
195
|
+
@t.instructions.should=={:type=>"application/pdf", :name=>"instructions.pdf", :size=>20956}
|
196
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/instructions.pdf').should==true
|
197
|
+
@t.update(:instructions=>"")
|
198
|
+
@t.instructions.should=={:type=>"application/pdf", :name=>"instructions.pdf", :size=>20956}
|
199
|
+
F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/instructions.pdf').should==true
|
200
|
+
end
|
201
|
+
|
202
|
+
it "Should have ImageMagick string builder" do
|
203
|
+
@t = Treasure.create(:map=>@img)
|
204
|
+
|
205
|
+
@t.image_magick(:map, 'test.gif') do
|
206
|
+
im_write("-negate")
|
207
|
+
im_crop(200,100,20,10)
|
208
|
+
im_resize(nil, 100)
|
209
|
+
end.should=="-negate -crop 200x100+20+10 +repage -resize 'x100'"
|
210
|
+
F.exists?(@t.file_url(:map,'test.gif',true)).should==true
|
211
|
+
|
212
|
+
@t.image_magick(:map, 'test2.gif') do
|
213
|
+
im_write("-negate")
|
214
|
+
im_crop(200,100,20,10)
|
215
|
+
im_resize(nil, 100, '>')
|
216
|
+
end.should=="-negate -crop 200x100+20+10 +repage -resize 'x100>'"
|
217
|
+
F.exists?(@t.file_url(:map,'test2.gif',true)).should==true
|
218
|
+
|
219
|
+
@t.image_magick(:map, 'test3.gif') do
|
220
|
+
im_write("-negate")
|
221
|
+
im_crop(200,100,20,10)
|
222
|
+
im_resize(200, 100, '^')
|
223
|
+
end.should=="-negate -crop 200x100+20+10 +repage -resize '200x100^' -gravity center -extent 200x100"
|
224
|
+
F.exists?(@t.file_url(:map,'test3.gif',true)).should==true
|
225
|
+
|
226
|
+
@t.image_magick(:map, 'test4.gif') do
|
227
|
+
im_write("-negate")
|
228
|
+
im_crop(200,100,20,10)
|
229
|
+
im_resize(200, 100, '^', 'North')
|
230
|
+
end.should=="-negate -crop 200x100+20+10 +repage -resize '200x100^' -gravity North -extent 200x100"
|
231
|
+
F.exists?(@t.file_url(:map,'test4.gif',true)).should==true
|
232
|
+
|
233
|
+
end
|
234
|
+
|
235
|
+
::FileUtils.rm_rf(Treasure::PUBLIC) if F.exists?(Treasure::PUBLIC)
|
236
|
+
|
237
|
+
end
|
data/stash_magic.gemspec
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'stash-magic'
|
3
|
+
s.version = "0.0.2"
|
4
|
+
s.platform = Gem::Platform::RUBY
|
5
|
+
s.summary = "Simple Attachment Manager"
|
6
|
+
s.description = "A simple attachment system that also handles thumbnails or other styles via ImageMagick. Originaly tested on Sequel ORM but purposedly easy to plug to something else."
|
7
|
+
s.files = `git ls-files`.split("\n").sort
|
8
|
+
s.test_files = ['spec.rb']
|
9
|
+
s.require_path = '.'
|
10
|
+
s.author = "Mickael Riga"
|
11
|
+
s.email = "mig@mypeplum.com"
|
12
|
+
s.homepage = "http://github.com/mig-hub/stash_magic"
|
13
|
+
end
|
data/stash_magic.rb
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
# A replacement for our current attachment system
|
5
|
+
# New requirements being:
|
6
|
+
# - More than one attachment per model
|
7
|
+
# - Easiest way to deal with folders (a bit like on our internal blog: the_wall)
|
8
|
+
# - Another way to deal with convert styles so that you can interract with it after saving the images (cropping for example)
|
9
|
+
# - Some facilities for pre-defined ImageMagick scripts
|
10
|
+
module StashMagic
|
11
|
+
|
12
|
+
F = ::File
|
13
|
+
D = ::Dir
|
14
|
+
FU = ::FileUtils
|
15
|
+
|
16
|
+
def self.included(into)
|
17
|
+
class << into
|
18
|
+
attr_reader :public_root
|
19
|
+
attr_accessor :stash_reflection
|
20
|
+
# Setter
|
21
|
+
def public_root=(location)
|
22
|
+
@public_root = location
|
23
|
+
FU.mkdir_p(location+'/stash/'+self.name.to_s)
|
24
|
+
end
|
25
|
+
# Declare a stash entry
|
26
|
+
def stash(name, options={})
|
27
|
+
stash_reflection.store name.to_sym, options
|
28
|
+
# Exemple of upload hash for attachments:
|
29
|
+
# { :type=>"image/jpeg",
|
30
|
+
# :filename=>"default.jpeg",
|
31
|
+
# :tempfile=>#<File:/var/folders/J0/J03dF6-7GCyxMhaB17F5yk+++TI/-Tmp-/RackMultipart.12704.0>,
|
32
|
+
# :head=>"Content-Disposition: form-data; name=\"model[attachment]\"; filename=\"default.jpeg\"\r\nContent-Type: image/jpeg\r\n",
|
33
|
+
# :name=>"model[attachment]"
|
34
|
+
# }
|
35
|
+
#
|
36
|
+
# GETTER
|
37
|
+
define_method name.to_s+'=' do |upload_hash|
|
38
|
+
return if upload_hash=="" # File in the form is unchanged
|
39
|
+
|
40
|
+
if upload_hash.nil?
|
41
|
+
destroy_files_for(name)
|
42
|
+
super('')
|
43
|
+
else
|
44
|
+
|
45
|
+
@tempfile_path ||= {}
|
46
|
+
@tempfile_path[name.to_sym] = upload_hash[:tempfile].path
|
47
|
+
h = {
|
48
|
+
:name => name.to_s + upload_hash[:filename][/\.[^.]+$/],
|
49
|
+
:type => upload_hash[:type],
|
50
|
+
:size => upload_hash[:tempfile].size
|
51
|
+
}
|
52
|
+
super(h.inspect)
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
# SETTER
|
57
|
+
define_method name.to_s do
|
58
|
+
eval(super.to_s)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
into.stash_reflection = {}
|
64
|
+
end
|
65
|
+
|
66
|
+
# Sugar
|
67
|
+
def public_root
|
68
|
+
self.class.public_root
|
69
|
+
end
|
70
|
+
|
71
|
+
# This method the path for images of a specific style(original by default)
|
72
|
+
# The argument 'full' means it returns the absolute path(used to save files)
|
73
|
+
# This could be a private method only used by file_url, but i keep it public just in case
|
74
|
+
def file_path(full=false)
|
75
|
+
raise "#{self.class}.public_root is not declared" if public_root.nil?
|
76
|
+
"#{public_root if full}/stash/#{self.class.to_s}/#{self.id || 'tmp'}"
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns the url of an attachment in a specific style(original if nil)
|
80
|
+
# The argument 'full' means it returns the absolute path(used to save files)
|
81
|
+
def file_url(attachment_name, style=nil, full=false)
|
82
|
+
f = send(attachment_name)
|
83
|
+
return nil if f.nil?
|
84
|
+
fn = style.nil? ? f[:name] : "#{attachment_name}.#{style}"
|
85
|
+
"#{file_path(full)}/#{fn}"
|
86
|
+
end
|
87
|
+
|
88
|
+
# Build the image tag with all SEO friendly info
|
89
|
+
# It's possible to add html attributes in a hash
|
90
|
+
def build_image_tag(attachment_name, style=nil, html_attributes={})
|
91
|
+
title = send(attachment_name+'_tooltip') rescue nil
|
92
|
+
alt = send(attachment_name+'_alternative_text') rescue nil
|
93
|
+
html_attributes = {:src => file_url(attachment_name, style), :title => title, :alt => alt}.update(html_attributes)
|
94
|
+
html_attributes = html_attributes.map do |k,v|
|
95
|
+
%{#{k.to_s}="#{ERB::Util.h(v.to_s)}"}
|
96
|
+
end.join(' ')
|
97
|
+
|
98
|
+
"<img #{html_attributes} />"
|
99
|
+
end
|
100
|
+
|
101
|
+
# ===============
|
102
|
+
# = ImageMagick =
|
103
|
+
# ===============
|
104
|
+
# Basic
|
105
|
+
def convert(attachment_name, convert_steps="-resize 100x75^ -gravity center -extent 100x75", style='stash_thumb.gif')
|
106
|
+
system "convert \"#{file_url(attachment_name, nil, true)}\" #{convert_steps} \"#{file_url(attachment_name, style, true)}\""
|
107
|
+
end
|
108
|
+
# IM String builder
|
109
|
+
def image_magick(attachment_name, style, &block)
|
110
|
+
@image_magick_strings = []
|
111
|
+
instance_eval &block
|
112
|
+
convert_string = @image_magick_strings.join(' ')
|
113
|
+
convert(attachment_name, convert_string, style)
|
114
|
+
@image_magick_strings = nil
|
115
|
+
convert_string
|
116
|
+
end
|
117
|
+
def im_write(s)
|
118
|
+
@image_magick_strings << s
|
119
|
+
end
|
120
|
+
def im_resize(width, height, geometry_option=nil, gravity=nil)
|
121
|
+
if width.nil? || height.nil?
|
122
|
+
@image_magick_strings << "-resize '#{width}x#{height}#{geometry_option}'"
|
123
|
+
else
|
124
|
+
@image_magick_strings << "-resize '#{width}x#{height}#{geometry_option}' -gravity #{gravity || 'center'} -extent #{width}x#{height}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
def im_crop(width, height, x, y)
|
128
|
+
@image_magick_strings << "-crop #{width}x#{height}+#{x}+#{y} +repage"
|
129
|
+
end
|
130
|
+
def im_negate
|
131
|
+
@image_magick_strings << '-negate'
|
132
|
+
end
|
133
|
+
# ===================
|
134
|
+
# = End ImageMagick =
|
135
|
+
# ===================
|
136
|
+
|
137
|
+
def after_save
|
138
|
+
super rescue nil
|
139
|
+
unless (@tempfile_path.nil? || @tempfile_path.empty?)
|
140
|
+
stash_path = file_path(true)
|
141
|
+
D::mkdir(stash_path) unless F::exist?(stash_path)
|
142
|
+
@tempfile_path.each do |k,v|
|
143
|
+
url = file_url(k, nil, true)
|
144
|
+
destroy_files_for(k, url) # Destroy previously saved files
|
145
|
+
FU.move(v, url) # Save the new one
|
146
|
+
FU.chmod(0777, url)
|
147
|
+
after_stash(k)
|
148
|
+
end
|
149
|
+
# Reset in case we access to times the entry in the same session
|
150
|
+
# Like setting an attachment and destroying it consecutively
|
151
|
+
# Dummy ex: Model.create(:img => file).update(:img => nil)
|
152
|
+
@tempfile_path = nil
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def after_stash(attachment_name)
|
157
|
+
current = self.send(attachment_name)
|
158
|
+
convert(attachment_name) if !current.nil? && current[:type][/^image\//]
|
159
|
+
end
|
160
|
+
|
161
|
+
def destroy_files_for(attachment_name, url=nil)
|
162
|
+
url ||= file_url(attachment_name, nil, true)
|
163
|
+
D[url.sub(/\.[^.]+$/, '.*')].each {|f| FU.rm(f) }
|
164
|
+
end
|
165
|
+
alias destroy_file_for destroy_files_for
|
166
|
+
|
167
|
+
def after_destroy
|
168
|
+
super rescue nil
|
169
|
+
p = file_path(true)
|
170
|
+
FU.rm_rf(p) if F.exists?(p)
|
171
|
+
end
|
172
|
+
|
173
|
+
class << self
|
174
|
+
# Include and declare public root in one go
|
175
|
+
def with_public_root(location, into=nil)
|
176
|
+
into ||= into_from_backtrace(caller)
|
177
|
+
into.__send__(:include, StashMagic)
|
178
|
+
into.public_root = location
|
179
|
+
into
|
180
|
+
end
|
181
|
+
# Trick stolen from Innate framework
|
182
|
+
# Allows not to pass self all the time
|
183
|
+
def into_from_backtrace(backtrace)
|
184
|
+
filename, lineno = backtrace[0].split(':', 2)
|
185
|
+
regexp = /^\s*class\s+(\S+)/
|
186
|
+
F.readlines(filename)[0..lineno.to_i].reverse.find{|ln| ln =~ regexp }
|
187
|
+
const_get($1)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stash-magic
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 2
|
10
|
+
version: 0.0.2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Mickael Riga
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-10-18 00:00:00 +01:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: A simple attachment system that also handles thumbnails or other styles via ImageMagick. Originaly tested on Sequel ORM but purposedly easy to plug to something else.
|
23
|
+
email: mig@mypeplum.com
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files: []
|
29
|
+
|
30
|
+
files:
|
31
|
+
- .gitignore
|
32
|
+
- MIT_LICENCE
|
33
|
+
- README.rdoc
|
34
|
+
- spec.rb
|
35
|
+
- stash_magic.gemspec
|
36
|
+
- stash_magic.rb
|
37
|
+
has_rdoc: true
|
38
|
+
homepage: http://github.com/mig-hub/stash_magic
|
39
|
+
licenses: []
|
40
|
+
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
|
44
|
+
require_paths:
|
45
|
+
- .
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
hash: 3
|
52
|
+
segments:
|
53
|
+
- 0
|
54
|
+
version: "0"
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 3
|
61
|
+
segments:
|
62
|
+
- 0
|
63
|
+
version: "0"
|
64
|
+
requirements: []
|
65
|
+
|
66
|
+
rubyforge_project:
|
67
|
+
rubygems_version: 1.3.7
|
68
|
+
signing_key:
|
69
|
+
specification_version: 3
|
70
|
+
summary: Simple Attachment Manager
|
71
|
+
test_files:
|
72
|
+
- spec.rb
|