stash-magic 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (7) hide show
  1. data/.gitignore +1 -0
  2. data/MIT_LICENCE +19 -0
  3. data/README.rdoc +213 -0
  4. data/spec.rb +237 -0
  5. data/stash_magic.gemspec +13 -0
  6. data/stash_magic.rb +191 -0
  7. 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
@@ -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