stash-magic 0.0.9 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/README.rdoc +64 -19
- data/lib/stash_magic/image_magick_string_builder.rb +31 -0
- data/lib/stash_magic/storage_filesystem.rb +74 -0
- data/lib/stash_magic/storage_s3.rb +108 -0
- data/lib/stash_magic.rb +164 -0
- data/stash_magic.gemspec +3 -4
- data/test/spec_builder.rb +105 -0
- data/{spec.rb → test/spec_filesystem.rb} +24 -47
- data/test/spec_s3.rb +258 -0
- metadata +15 -10
- data/stash_magic.rb +0 -198
data/.gitignore
CHANGED
data/README.rdoc
CHANGED
@@ -2,13 +2,15 @@
|
|
2
2
|
|
3
3
|
= Stash Magic (BETA)
|
4
4
|
|
5
|
-
Stash Magic provides a very simple interface for dealing with file system
|
5
|
+
Stash Magic provides a very simple interface for dealing with attachments on file system or Amazon S3 in a database and help you with thumbnails or other styles via ImageMagick (hence, the name). Features are:
|
6
6
|
|
7
7
|
- Many attachments per database entry
|
8
8
|
- ImageMagick string builder
|
9
|
-
- after_stash hook for creating thumbnails or other styles automatically when attachment is created or updated.
|
9
|
+
- `after_stash` hook for creating thumbnails or other styles automatically when attachment is created or updated.
|
10
|
+
- `after_stash` hook also allow you to recreate all styles when you changed one
|
10
11
|
- Specs for Sequel ORM but pretty easy to adapt
|
11
|
-
- Easy to understand (
|
12
|
+
- Easy to understand (due to the lack of bells and whistles which makes it more dangerous as well. Gniark gniark.)
|
13
|
+
- Storage on filesystem or Amazon S3
|
12
14
|
|
13
15
|
This is still in Beta version built with simplicity in mind.
|
14
16
|
It tries to do a lot with less code that anybody would be able to understand quickly.
|
@@ -20,16 +22,8 @@ So any test or help is welcome.
|
|
20
22
|
|
21
23
|
= How to install
|
22
24
|
|
23
|
-
gem install stash-magic
|
24
|
-
|
25
|
-
Use Sudo if you want to install it for all users of your server:
|
26
|
-
|
27
25
|
sudo gem install stash-magic
|
28
26
|
|
29
|
-
If you have Ruby 1.9, this is most likely that you have to replace the command gem by gem19:
|
30
|
-
|
31
|
-
sudo gem19 install stash-magic
|
32
|
-
|
33
27
|
= How to use
|
34
28
|
|
35
29
|
First you have to require the module:
|
@@ -42,12 +36,25 @@ And then inside your model class, you have to include the module and declare whe
|
|
42
36
|
include ::StashMagic
|
43
37
|
self.public_root = ::File.expand_path(::File.dirname(__FILE__)+'/public')
|
44
38
|
end
|
39
|
+
|
40
|
+
Or if you want to use Amazon S3, replace `public_root` by `bucket:
|
41
|
+
|
42
|
+
class Treasure < ::Sequel::Model
|
43
|
+
include ::StashMagic
|
44
|
+
self.public_root = ::File.expand_path(::File.dirname(__FILE__)+'/public')
|
45
|
+
end
|
45
46
|
|
46
|
-
The module has a method to
|
47
|
+
The module has a method to include and set it at once though:
|
47
48
|
|
48
49
|
class Treasure < ::Sequel::Model
|
49
50
|
::StashMagic.with_public_root ::File.expand_path(::File.dirname(__FILE__)+'/public')
|
50
51
|
end
|
52
|
+
|
53
|
+
Or for Amazon S3:
|
54
|
+
|
55
|
+
class Treasure < ::Sequel::Model
|
56
|
+
::StashMagic.bucket 'my-bucket-on-amazon-s3'
|
57
|
+
end
|
51
58
|
|
52
59
|
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:
|
53
60
|
|
@@ -58,7 +65,7 @@ After that, for each attachment you want, you need to have a column in the datab
|
|
58
65
|
stash :stamp
|
59
66
|
end
|
60
67
|
|
61
|
-
This method accepts an optional hash as a second argument which
|
68
|
+
This method accepts an optional hash as a second argument which could be handy for you as you can have it in the stash reflection:
|
62
69
|
|
63
70
|
class Treasure < ::Sequel::Model
|
64
71
|
::StashMagic.with_public_root ::File.expand_path(::File.dirname(__FILE__)+'/public')
|
@@ -73,6 +80,17 @@ The method Treasure.stash_reflection would return:
|
|
73
80
|
:map => {},
|
74
81
|
:stamp => {:accept_gif => false, :limit => 512000}
|
75
82
|
}
|
83
|
+
|
84
|
+
It is also used for setting Amazon S3 in order to declare the options when storing.
|
85
|
+
The key is `:s3_store_options`.
|
86
|
+
For instance you can declare an attachment to be private (StashMagic uses :public_read by default):
|
87
|
+
|
88
|
+
class Treasure < ::Sequel::Model
|
89
|
+
::StashMagic.with_public_root ::File.expand_path(::File.dirname(__FILE__)+'/public')
|
90
|
+
|
91
|
+
stash :map
|
92
|
+
stash :stamp, :s3_store_options => { :access => :private }
|
93
|
+
end
|
76
94
|
|
77
95
|
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:
|
78
96
|
|
@@ -91,10 +109,12 @@ When you want to use attachment in your application, you can retrieve the file u
|
|
91
109
|
@treasure_instance.file_url(:map) # The original file
|
92
110
|
@treasure_instance.file_url(:map, 'thumb.gif') # The picture in a thumb.gif style (see next chapter to learn about styles)
|
93
111
|
|
94
|
-
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
|
112
|
+
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 similar method `file_path` with a boolean. When set to true, it will give you the absolute path to the file (file system only):
|
113
|
+
|
114
|
+
@treasure_instance.file_path(:map, nil, true) # /absolute/path/to/public/stash/Treasure/1/map.pdf
|
115
|
+
@treasure_instance.file_path(:map, 'thumb.gif', true) # /absolute/path/to/public/stash/Treasure/1/map.thumb.gif
|
95
116
|
|
96
|
-
|
97
|
-
@treasure_instance.file_url(:map, 'thumb.gif', true) # /absolute/path/to/public/stash/Treasure/1/map.thumb.gif
|
117
|
+
When using Amazon S3, there is a 3rd argument to `file_url` which is a boolean that says if you want ssl or not (false by default). Because the `file_url` is an absolute path when using S3.
|
98
118
|
|
99
119
|
= Thumbnails and Other Styles
|
100
120
|
|
@@ -119,7 +139,7 @@ Even though StashMagic provides a builder for ImageMagick scripts, I suggest you
|
|
119
139
|
- The builder is limited, not as complete as real ImageMagick ruby wrapper like RMagick
|
120
140
|
- I like to believe it's fun as well (not only powerful)
|
121
141
|
|
122
|
-
So for the
|
142
|
+
So for the courageous amongst you, here is the way you create a very simple style for the portrait attachment:
|
123
143
|
|
124
144
|
@treasure_instance.convert :portrait, '-resize 100x75', 'mini.gif'
|
125
145
|
|
@@ -192,7 +212,7 @@ Which will secretly do something like:
|
|
192
212
|
|
193
213
|
= How to create thumbnails on the flight (The Hook)
|
194
214
|
|
195
|
-
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
|
215
|
+
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 automatically for every image a thumbnail called 'stash_thumb.gif'.
|
196
216
|
|
197
217
|
What you have to do is overwrite the hook. For example, say you want every attachment to have a 200x200 perfect squared version:
|
198
218
|
|
@@ -216,6 +236,24 @@ This is done by using `nil` as a style:
|
|
216
236
|
image_magick(attachment_name) { im_resize(400,300) }
|
217
237
|
end
|
218
238
|
|
239
|
+
= More about `after_stash` hook
|
240
|
+
|
241
|
+
If you need to reprocess all the images for a specific attachment, you can use the hook manually:
|
242
|
+
|
243
|
+
@my_instance.after_stash(:illustration)
|
244
|
+
|
245
|
+
Now if you want to do it on all the instances of the class:
|
246
|
+
|
247
|
+
MyModelClass.all{ |i| i.after_stash(:illustration) }
|
248
|
+
|
249
|
+
But you might want to do the same for all attachments:
|
250
|
+
|
251
|
+
MyModelClass.all{ |i| MyModelClass.stash_reflection.keys.each { |k| i.after_stash(k) } }
|
252
|
+
|
253
|
+
And finally there is a simple method that does the same for all the Classes using StashMagic:
|
254
|
+
|
255
|
+
StashMagic.all_after_stash
|
256
|
+
|
219
257
|
= How my files are then saved on my file system
|
220
258
|
|
221
259
|
I like to believe that one don't have to think about that as long as the module provides enough methods to do what you need to do.
|
@@ -247,13 +285,19 @@ Please note that the class name is a simple #to_s. I've realized recently that m
|
|
247
285
|
BootStrap
|
248
286
|
BootsTrap
|
249
287
|
|
288
|
+
Amazon S3 filenames follow the same sort of logic.
|
289
|
+
|
290
|
+
= Reprocess your thumbnails
|
291
|
+
|
292
|
+
Sometimes you need to change your `after_stash`
|
293
|
+
|
250
294
|
= More Details
|
251
295
|
|
252
296
|
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
|
253
297
|
|
254
298
|
The project is speced with
|
255
299
|
- Bacon 1.1.0
|
256
|
-
- Sequel 3.
|
300
|
+
- Sequel 3.19.0
|
257
301
|
- ImageMagick 6.5.8
|
258
302
|
|
259
303
|
= Change Log
|
@@ -265,6 +309,7 @@ The project is speced with
|
|
265
309
|
- 0.0.7 Make it possible to overwrite the original image with another style in after_stash
|
266
310
|
- 0.0.8 Default thumb does not break when file name has signs in the name
|
267
311
|
- 0.0.9 Fix a bug when Model#build_image_tag uses a symbol for the attachment name
|
312
|
+
- 0.1.0 Now with the option to use S3 instead of file system and a method to reprocess thumbnails
|
268
313
|
|
269
314
|
== Copyright
|
270
315
|
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module StashMagic
|
2
|
+
module ImageMagickStringBuilder
|
3
|
+
|
4
|
+
def image_magick(attachment_name, style=nil, &block)
|
5
|
+
@image_magick_strings = []
|
6
|
+
instance_eval &block
|
7
|
+
convert_string = @image_magick_strings.join(' ')
|
8
|
+
convert(attachment_name, convert_string, style)
|
9
|
+
@image_magick_strings = nil
|
10
|
+
convert_string
|
11
|
+
end
|
12
|
+
|
13
|
+
def im_write(s)
|
14
|
+
@image_magick_strings << s
|
15
|
+
end
|
16
|
+
def im_resize(width, height, geometry_option=nil, gravity=nil)
|
17
|
+
if width.nil? || height.nil?
|
18
|
+
@image_magick_strings << "-resize '#{width}x#{height}#{geometry_option}'"
|
19
|
+
else
|
20
|
+
@image_magick_strings << "-resize '#{width}x#{height}#{geometry_option}' -gravity #{gravity || 'center'} -extent #{width}x#{height}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
def im_crop(width, height, x, y)
|
24
|
+
@image_magick_strings << "-crop #{width}x#{height}+#{x}+#{y} +repage"
|
25
|
+
end
|
26
|
+
def im_negate
|
27
|
+
@image_magick_strings << '-negate'
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module StashMagic
|
2
|
+
module StorageFilesystem
|
3
|
+
|
4
|
+
# ===========
|
5
|
+
# = Helpers =
|
6
|
+
# ===========
|
7
|
+
|
8
|
+
def public_root; self.class.public_root; end
|
9
|
+
|
10
|
+
# This is the path of the instance
|
11
|
+
# Full is when you want a computer path as opposed to a browser path
|
12
|
+
def file_root(full=false)
|
13
|
+
"#{public_root if full}/stash/#{self.class.to_s}/#{self.id || 'tmp'}"
|
14
|
+
end
|
15
|
+
|
16
|
+
# This is the path of an attachment in a special style
|
17
|
+
# Full is when you want a computer path as opposed to a browser path
|
18
|
+
def file_path(attachment_name, style=nil, full=false)
|
19
|
+
f = __send__(attachment_name)
|
20
|
+
return nil if f.nil?
|
21
|
+
fn = style.nil? ? f[:name] : "#{attachment_name}.#{style}"
|
22
|
+
"#{file_root(full)}/#{fn}"
|
23
|
+
end
|
24
|
+
|
25
|
+
# The actual URL for a link to the attachment
|
26
|
+
# Here it is the same as file_path
|
27
|
+
# But that gives a unified version for filesystem and S3
|
28
|
+
def file_url(attachment_name, style=nil); file_path(attachment_name, style); end
|
29
|
+
|
30
|
+
# =========
|
31
|
+
# = Hooks =
|
32
|
+
# =========
|
33
|
+
|
34
|
+
def after_save
|
35
|
+
super rescue nil
|
36
|
+
unless (@tempfile_path.nil? || @tempfile_path.empty?)
|
37
|
+
stash_path = file_root(true)
|
38
|
+
D::mkdir(stash_path) unless F::exist?(stash_path)
|
39
|
+
@tempfile_path.each do |k,v|
|
40
|
+
url = file_path(k, nil, true)
|
41
|
+
destroy_files_for(k, url) # Destroy previously saved files
|
42
|
+
FU.move(v, url) # Save the new one
|
43
|
+
FU.chmod(0777, url)
|
44
|
+
after_stash(k)
|
45
|
+
end
|
46
|
+
# Reset in case we access two times the entry in the same session
|
47
|
+
# Like setting an attachment and destroying it in a row
|
48
|
+
# Dummy ex: Model.create(:img => file).update(:img => nil)
|
49
|
+
@tempfile_path = nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def destroy_files_for(attachment_name, url=nil)
|
54
|
+
url ||= file_path(attachment_name, nil, true)
|
55
|
+
D[url.sub(/\.[^.]+$/, '.*')].each {|f| FU.rm(f) }
|
56
|
+
end
|
57
|
+
alias destroy_file_for destroy_files_for
|
58
|
+
|
59
|
+
def after_destroy
|
60
|
+
super rescue nil
|
61
|
+
p = file_root(true)
|
62
|
+
FU.rm_rf(p) if F.exists?(p)
|
63
|
+
end
|
64
|
+
|
65
|
+
# ===============
|
66
|
+
# = ImageMagick =
|
67
|
+
# ===============
|
68
|
+
|
69
|
+
def convert(attachment_name, convert_steps="", style=nil)
|
70
|
+
system "convert \"#{file_path(attachment_name, nil, true)}\" #{convert_steps} \"#{file_path(attachment_name, style, true)}\""
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module StashMagic
|
2
|
+
module StorageS3
|
3
|
+
|
4
|
+
require 'aws/s3'
|
5
|
+
|
6
|
+
# ===========
|
7
|
+
# = Helpers =
|
8
|
+
# ===========
|
9
|
+
|
10
|
+
def bucket; self.class.bucket; end
|
11
|
+
|
12
|
+
def s3_store_options(attachment_name)
|
13
|
+
{:access=>:public_read}.update(self.class.stash_reflection[attachment_name][:s3_store_options]||{})
|
14
|
+
end
|
15
|
+
|
16
|
+
# This is the path of the instance
|
17
|
+
def file_root; "#{self.class.to_s}/#{self.id || 'tmp'}"; end
|
18
|
+
|
19
|
+
# This is the path of an attachment in a special style
|
20
|
+
def file_path(attachment_name, style=nil)
|
21
|
+
f = __send__(attachment_name)
|
22
|
+
return nil if f.nil?
|
23
|
+
fn = style.nil? ? f[:name] : "#{attachment_name}.#{style}"
|
24
|
+
"#{file_root}/#{fn}"
|
25
|
+
end
|
26
|
+
|
27
|
+
# URL to access the file in browser
|
28
|
+
# But it only gives a URL with no credentials, when the file is public
|
29
|
+
# Otherwise it is better to use Model#s3object.url
|
30
|
+
# Careful with the later if you have private files
|
31
|
+
def file_url(attachment_name, style=nil, ssl=false)
|
32
|
+
f = file_path(attachment_name, style)
|
33
|
+
return nil if f.nil?
|
34
|
+
"http#{'s' if ssl}://s3.amazonaws.com/#{bucket}/#{f}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def s3object(attachment_name,style=nil)
|
38
|
+
u = file_path(attachment_name,style)
|
39
|
+
return nil if u.nil?
|
40
|
+
AWS::S3::S3Object.find(u, bucket)
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_file(attachment_name, style=nil)
|
44
|
+
u = file_path(attachment_name,style)
|
45
|
+
f = Tempfile.new('StashMagic_src')
|
46
|
+
f.binmode
|
47
|
+
f.write(AWS::S3::S3Object.value(u, bucket))
|
48
|
+
f.rewind
|
49
|
+
f
|
50
|
+
end
|
51
|
+
|
52
|
+
# =========
|
53
|
+
# = Hooks =
|
54
|
+
# =========
|
55
|
+
|
56
|
+
def after_save
|
57
|
+
super rescue nil
|
58
|
+
unless (@tempfile_path.nil? || @tempfile_path.empty?)
|
59
|
+
stash_path = file_root
|
60
|
+
@tempfile_path.each do |k,v|
|
61
|
+
url = file_path(k, nil)
|
62
|
+
destroy_files_for(k, url) # Destroy previously saved files
|
63
|
+
AWS::S3::S3Object.store(url, open(v), bucket, s3_store_options(k))
|
64
|
+
after_stash(k)
|
65
|
+
end
|
66
|
+
# Reset in case we access two times the entry in the same session
|
67
|
+
# Like setting an attachment and destroying it in a row
|
68
|
+
# Dummy ex: Model.create(:img => file).update(:img => nil)
|
69
|
+
@tempfile_path = nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def destroy_files_for(attachment_name, url=nil)
|
74
|
+
url ||= file_path(attachment_name, nil)
|
75
|
+
AWS::S3::Bucket.objects(bucket, :prefix=>url.sub(/[^.]+$/, '')).each do |o|
|
76
|
+
o.delete
|
77
|
+
end
|
78
|
+
end
|
79
|
+
alias destroy_file_for destroy_files_for
|
80
|
+
|
81
|
+
def after_destroy
|
82
|
+
super rescue nil
|
83
|
+
AWS::S3::Bucket.objects(bucket, :prefix=>file_root).each do |o|
|
84
|
+
o.delete
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# ===============
|
89
|
+
# = ImageMagick =
|
90
|
+
# ===============
|
91
|
+
|
92
|
+
def convert(attachment_name, convert_steps="", style=nil)
|
93
|
+
@tempfile_path ||= {}
|
94
|
+
tempfile_path = @tempfile_path[attachment_name.to_sym]
|
95
|
+
if !tempfile_path.nil? && F.exists?(tempfile_path)
|
96
|
+
src_path = tempfile_path
|
97
|
+
else
|
98
|
+
src_path = get_file(attachment_name).path
|
99
|
+
end
|
100
|
+
dest = Tempfile.new('StashMagic_dest')
|
101
|
+
dest.binmode
|
102
|
+
dest.close
|
103
|
+
system "convert \"#{src_path}\" #{convert_steps} \"#{dest.path}\""
|
104
|
+
AWS::S3::S3Object.store(file_path(attachment_name,style), dest.open, bucket, s3_store_options(attachment_name))
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
data/lib/stash_magic.rb
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'stash_magic/image_magick_string_builder'
|
3
|
+
require 'stash_magic/storage_filesystem'
|
4
|
+
require 'stash_magic/storage_s3'
|
5
|
+
|
6
|
+
module StashMagic
|
7
|
+
|
8
|
+
F = ::File
|
9
|
+
D = ::Dir
|
10
|
+
FU = ::FileUtils
|
11
|
+
include ImageMagickStringBuilder
|
12
|
+
|
13
|
+
# ====================
|
14
|
+
# = Module Inclusion =
|
15
|
+
# ====================
|
16
|
+
|
17
|
+
class << self
|
18
|
+
attr_accessor :classes
|
19
|
+
StashMagic.classes = []
|
20
|
+
|
21
|
+
# Include and declare public root in one go
|
22
|
+
def with_public_root(location, into=nil)
|
23
|
+
into ||= into_from_backtrace(caller)
|
24
|
+
into.__send__(:include, self)
|
25
|
+
into.public_root = location
|
26
|
+
into
|
27
|
+
end
|
28
|
+
# Include and declare bucket in one go
|
29
|
+
def with_bucket(bucket, into=nil)
|
30
|
+
into ||= into_from_backtrace(caller)
|
31
|
+
into.__send__(:include, self)
|
32
|
+
into.bucket = bucket
|
33
|
+
into
|
34
|
+
end
|
35
|
+
# Trick stolen from Innate framework
|
36
|
+
# Allows not to pass self all the time
|
37
|
+
def into_from_backtrace(backtrace)
|
38
|
+
filename, lineno = backtrace[0].split(':', 2)
|
39
|
+
regexp = /^\s*class\s+(\S+)/
|
40
|
+
F.readlines(filename)[0..lineno.to_i].reverse.find{|ln| ln =~ regexp }
|
41
|
+
const_get($1)
|
42
|
+
end
|
43
|
+
|
44
|
+
def all_after_stash
|
45
|
+
StashMagic.classes.each do |m|
|
46
|
+
m.all do |i|
|
47
|
+
m.stash_reflection.keys.each do |k|
|
48
|
+
i.after_stash(k)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
# ============
|
57
|
+
# = Included =
|
58
|
+
# ============
|
59
|
+
|
60
|
+
def self.included(into)
|
61
|
+
|
62
|
+
StashMagic.classes << into
|
63
|
+
|
64
|
+
class << into
|
65
|
+
attr_accessor :stash_reflection, :storage
|
66
|
+
attr_reader :public_root, :bucket
|
67
|
+
|
68
|
+
#include(ImageMagickStringBuilder)
|
69
|
+
|
70
|
+
def public_root=(location)
|
71
|
+
@public_root = location
|
72
|
+
FU.mkdir_p(location+'/stash/'+self.name.to_s)
|
73
|
+
include(StorageFilesystem)
|
74
|
+
@storage = :filesystem
|
75
|
+
end
|
76
|
+
|
77
|
+
def bucket=(b)
|
78
|
+
@bucket = b.to_s
|
79
|
+
include(StorageS3)
|
80
|
+
@storage = :s3
|
81
|
+
end
|
82
|
+
|
83
|
+
# Declare a stash entry
|
84
|
+
def stash(name, options={})
|
85
|
+
stash_reflection.store name.to_sym, options
|
86
|
+
# Exemple of upload hash for attachments:
|
87
|
+
# { :type=>"image/jpeg",
|
88
|
+
# :filename=>"default.jpeg",
|
89
|
+
# :tempfile=>#<File:/var/folders/J0/J03dF6-7GCyxMhaB17F5yk+++TI/-Tmp-/RackMultipart.12704.0>,
|
90
|
+
# :head=>"Content-Disposition: form-data; name=\"model[attachment]\"; filename=\"default.jpeg\"\r\nContent-Type: image/jpeg\r\n",
|
91
|
+
# :name=>"model[attachment]"
|
92
|
+
# }
|
93
|
+
#
|
94
|
+
# SETTER
|
95
|
+
define_method name.to_s+'=' do |upload_hash|
|
96
|
+
return if upload_hash=="" # File in the form is unchanged
|
97
|
+
|
98
|
+
if upload_hash.nil?
|
99
|
+
destroy_files_for(name) unless self.__send__(name).nil?
|
100
|
+
super('')
|
101
|
+
else
|
102
|
+
|
103
|
+
@tempfile_path ||= {}
|
104
|
+
@tempfile_path[name.to_sym] = upload_hash[:tempfile].path
|
105
|
+
h = {
|
106
|
+
:name => name.to_s + upload_hash[:filename][/\.[^.]+$/],
|
107
|
+
:type => upload_hash[:type],
|
108
|
+
:size => upload_hash[:tempfile].size
|
109
|
+
}
|
110
|
+
super(h.inspect)
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
114
|
+
# GETTER
|
115
|
+
define_method name.to_s do |*args|
|
116
|
+
eval(super(*args).to_s)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
into.stash_reflection = {}
|
122
|
+
end
|
123
|
+
|
124
|
+
# ===========
|
125
|
+
# = Helpers =
|
126
|
+
# ===========
|
127
|
+
|
128
|
+
# Build the image tag with all SEO friendly info
|
129
|
+
# It's possible to add html attributes in a hash
|
130
|
+
def build_image_tag(attachment_name, style=nil, html_attributes={})
|
131
|
+
title_field, alt_field = (attachment_name.to_s+'_tooltip').to_sym, (attachment_name.to_s+'_alternative_text').to_sym
|
132
|
+
title = __send__(title_field) if columns.include?(title_field)
|
133
|
+
alt = __send__(alt_field) if columns.include?(alt_field)
|
134
|
+
html_attributes = {:src => file_url(attachment_name, style), :title => title, :alt => alt}.update(html_attributes)
|
135
|
+
html_attributes = html_attributes.map do |k,v|
|
136
|
+
%{#{k.to_s}="#{html_escape(v.to_s)}"}
|
137
|
+
end.join(' ')
|
138
|
+
|
139
|
+
"<img #{html_attributes} />"
|
140
|
+
end
|
141
|
+
|
142
|
+
# =========
|
143
|
+
# = Hooks =
|
144
|
+
# =========
|
145
|
+
|
146
|
+
def after_stash(attachment_name)
|
147
|
+
current = self.__send__(attachment_name)
|
148
|
+
convert(attachment_name, "-resize '100x75^' -gravity center -extent 100x75", 'stash_thumb.gif') if !current.nil? && current[:type][/^image\//]
|
149
|
+
end
|
150
|
+
|
151
|
+
def method_missing(m,*args)
|
152
|
+
raise(NoMethodError, "You have to choose a strorage system") if self.class.storage.nil?
|
153
|
+
super
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
# Stolen from ERB
|
159
|
+
def html_escape(s)
|
160
|
+
s.to_s.gsub(/&/, "&").gsub(/\"/, """).gsub(/>/, ">").gsub(/</, "<")
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
end
|
data/stash_magic.gemspec
CHANGED
@@ -1,12 +1,11 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'stash-magic'
|
3
|
-
s.version = "0.0
|
3
|
+
s.version = "0.1.0"
|
4
4
|
s.platform = Gem::Platform::RUBY
|
5
5
|
s.summary = "File Attachment Made Simple"
|
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."
|
6
|
+
s.description = "A simple attachment system (file system or Amazon S3) that also handles thumbnails or other styles via ImageMagick. Originaly tested on Sequel ORM but purposedly easy to plug to something else."
|
7
7
|
s.files = `git ls-files`.split("\n").sort
|
8
|
-
s.
|
9
|
-
s.require_path = '.'
|
8
|
+
s.require_path = './lib'
|
10
9
|
s.author = "Mickael Riga"
|
11
10
|
s.email = "mig@mypeplum.com"
|
12
11
|
s.homepage = "http://github.com/mig-hub/stash_magic"
|
@@ -0,0 +1,105 @@
|
|
1
|
+
F = ::File
|
2
|
+
D = ::Dir
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'bacon'
|
6
|
+
|
7
|
+
require 'sequel'
|
8
|
+
DB = Sequel.sqlite
|
9
|
+
|
10
|
+
require 'tempfile'
|
11
|
+
|
12
|
+
$:.unshift(F.dirname(__FILE__)+'/../lib')
|
13
|
+
require 'stash_magic'
|
14
|
+
|
15
|
+
class Treasure < ::Sequel::Model
|
16
|
+
PUBLIC = F.expand_path(F.dirname(__FILE__)+'/public')
|
17
|
+
::StashMagic.with_public_root(PUBLIC)
|
18
|
+
|
19
|
+
plugin :schema
|
20
|
+
set_schema do
|
21
|
+
primary_key :id
|
22
|
+
Integer :age
|
23
|
+
String :map # jpeg
|
24
|
+
String :map_tooltip
|
25
|
+
String :map_alternative_text
|
26
|
+
String :mappy # jpeg - Used to see if mappy files are not destroyed when map is (because it starts the same)
|
27
|
+
String :instructions #pdf
|
28
|
+
end
|
29
|
+
create_table unless table_exists?
|
30
|
+
|
31
|
+
stash :map
|
32
|
+
stash :mappy
|
33
|
+
stash :instructions
|
34
|
+
|
35
|
+
def validate
|
36
|
+
errors[:age] << "Not old enough" unless (self.age.nil? || self.age>10)
|
37
|
+
errors[:instructions] << "Too big" if (!self.instructions.nil? && self.instructions[:size].to_i>46000)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Make temporary public folder
|
42
|
+
D.mkdir(Treasure::PUBLIC) unless F.exists?(Treasure::PUBLIC)
|
43
|
+
|
44
|
+
describe 'StashMagic ImageMagickStringBuilder' do
|
45
|
+
|
46
|
+
`convert rose: #{Treasure::PUBLIC}/rose.jpg` unless F.exists?(Treasure::PUBLIC+'/rose.jpg') # Use ImageMagick to build a tmp image to use
|
47
|
+
`convert granite: #{Treasure::PUBLIC}/granite.gif` unless F.exists?(Treasure::PUBLIC+'/granite.gif') # Use ImageMagick to build a tmp image to use
|
48
|
+
`convert rose: #{Treasure::PUBLIC}/rose.pdf` unless F.exists?(Treasure::PUBLIC+'/rose.pdf') # Use ImageMagick to build a tmp image to use
|
49
|
+
`convert logo: #{Treasure::PUBLIC}/logo.pdf` unless F.exists?(Treasure::PUBLIC+'/logo.pdf')
|
50
|
+
|
51
|
+
def mock_upload(uploaded_file_path, content_type, binary=false)
|
52
|
+
n = F.basename(uploaded_file_path)
|
53
|
+
f = ::Tempfile.new(n)
|
54
|
+
f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
|
55
|
+
f.binmode if binary
|
56
|
+
::FileUtils.copy_file(uploaded_file_path, f.path)
|
57
|
+
{
|
58
|
+
:filename => n,
|
59
|
+
:type => content_type,
|
60
|
+
:tempfile => f
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
before do
|
65
|
+
@img = mock_upload(Treasure::PUBLIC+'/rose.jpg', 'image/jpeg', true)
|
66
|
+
@img2 = mock_upload(Treasure::PUBLIC+'/granite.gif', 'image/gif', true)
|
67
|
+
@pdf = mock_upload(Treasure::PUBLIC+'/rose.pdf', 'application/pdf', true)
|
68
|
+
@pdf2 = mock_upload(Treasure::PUBLIC+'/logo.pdf', 'application/pdf', true)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "Should have ImageMagick building strings correctly" do
|
72
|
+
@t = Treasure.create(:map=>@img)
|
73
|
+
|
74
|
+
@t.image_magick(:map, 'test.gif') do
|
75
|
+
im_write("-negate")
|
76
|
+
im_crop(200,100,20,10)
|
77
|
+
im_resize(nil, 100)
|
78
|
+
end.should=="-negate -crop 200x100+20+10 +repage -resize 'x100'"
|
79
|
+
F.exists?(@t.file_path(:map,'test.gif',true)).should==true
|
80
|
+
|
81
|
+
@t.image_magick(:map, 'test2.gif') do
|
82
|
+
im_write("-negate")
|
83
|
+
im_crop(200,100,20,10)
|
84
|
+
im_resize(nil, 100, '>')
|
85
|
+
end.should=="-negate -crop 200x100+20+10 +repage -resize 'x100>'"
|
86
|
+
F.exists?(@t.file_path(:map,'test2.gif',true)).should==true
|
87
|
+
|
88
|
+
@t.image_magick(:map, 'test3.gif') do
|
89
|
+
im_write("-negate")
|
90
|
+
im_crop(200,100,20,10)
|
91
|
+
im_resize(200, 100, '^')
|
92
|
+
end.should=="-negate -crop 200x100+20+10 +repage -resize '200x100^' -gravity center -extent 200x100"
|
93
|
+
F.exists?(@t.file_path(:map,'test3.gif',true)).should==true
|
94
|
+
|
95
|
+
@t.image_magick(:map, 'test4.gif') do
|
96
|
+
im_write("-negate")
|
97
|
+
im_crop(200,100,20,10)
|
98
|
+
im_resize(200, 100, '^', 'North')
|
99
|
+
end.should=="-negate -crop 200x100+20+10 +repage -resize '200x100^' -gravity North -extent 200x100"
|
100
|
+
F.exists?(@t.file_path(:map,'test4.gif',true)).should==true
|
101
|
+
end
|
102
|
+
|
103
|
+
::FileUtils.rm_rf(Treasure::PUBLIC) if F.exists?(Treasure::PUBLIC)
|
104
|
+
|
105
|
+
end
|
@@ -1,20 +1,16 @@
|
|
1
|
-
# =========
|
2
|
-
# = Setup =
|
3
|
-
# =========
|
4
|
-
|
5
1
|
F = ::File
|
6
2
|
D = ::Dir
|
7
3
|
|
8
4
|
require 'rubygems'
|
9
5
|
require 'bacon'
|
10
|
-
Bacon.summary_on_exit
|
11
6
|
|
12
7
|
require 'sequel'
|
13
8
|
DB = Sequel.sqlite
|
14
9
|
|
15
10
|
require 'tempfile'
|
16
11
|
|
17
|
-
|
12
|
+
$:.unshift(F.dirname(__FILE__)+'/../lib')
|
13
|
+
require 'stash_magic'
|
18
14
|
|
19
15
|
class Treasure < ::Sequel::Model
|
20
16
|
PUBLIC = F.expand_path(F.dirname(__FILE__)+'/public')
|
@@ -61,7 +57,7 @@ D.mkdir(Treasure::PUBLIC) unless F.exists?(Treasure::PUBLIC)
|
|
61
57
|
# = Tests =
|
62
58
|
# =========
|
63
59
|
|
64
|
-
describe
|
60
|
+
describe 'StashMagic Filesystem' do
|
65
61
|
|
66
62
|
`convert rose: #{Treasure::PUBLIC}/rose.jpg` unless F.exists?(Treasure::PUBLIC+'/rose.jpg') # Use ImageMagick to build a tmp image to use
|
67
63
|
`convert granite: #{Treasure::PUBLIC}/granite.gif` unless F.exists?(Treasure::PUBLIC+'/granite.gif') # Use ImageMagick to build a tmp image to use
|
@@ -96,26 +92,31 @@ describe ::StashMagic do
|
|
96
92
|
F.exists?(Treasure::PUBLIC+'/stash/Treasure').should==true
|
97
93
|
end
|
98
94
|
|
95
|
+
it 'Should keep a list of Classes that included it' do
|
96
|
+
StashMagic.classes.should==[Treasure, BadTreasure]
|
97
|
+
end
|
98
|
+
|
99
99
|
it "Should stash entries with Class::stash and have reflection" do
|
100
100
|
Treasure.stash_reflection.keys.include?(:map).should==true
|
101
101
|
Treasure.stash_reflection.keys.include?(:instructions).should==true
|
102
102
|
end
|
103
103
|
|
104
|
-
it "Should give instance its own
|
104
|
+
it "Should give instance its own file_root" do
|
105
105
|
# Normal path
|
106
106
|
@t = Treasure.create
|
107
|
-
@t.
|
107
|
+
@t.file_root.should=="/stash/Treasure/#{@t.id}"
|
108
108
|
# Anonymous path
|
109
|
-
Treasure.new.
|
109
|
+
Treasure.new.file_root.should=='/stash/Treasure/tmp'
|
110
110
|
# Normal path full
|
111
111
|
@t = Treasure.create
|
112
|
-
@t.
|
112
|
+
@t.file_root(true).should==Treasure::PUBLIC+"/stash/Treasure/#{@t.id}"
|
113
113
|
# Anonymous path full
|
114
|
-
Treasure.new.
|
114
|
+
Treasure.new.file_root(true).should==Treasure::PUBLIC+'/stash/Treasure/tmp'
|
115
115
|
end
|
116
116
|
|
117
|
-
it "Should
|
118
|
-
lambda {
|
117
|
+
it "Should warn you when you have no storage chosen" do
|
118
|
+
lambda { Treasure.new.zzz }.should.raise(NoMethodError).message.should.not=='You have to choose a strorage system'
|
119
|
+
lambda { BadTreasure.new.public_root }.should.raise(NoMethodError).message.should=='You have to choose a strorage system'
|
119
120
|
end
|
120
121
|
|
121
122
|
it "Should not raise on setters eval when value already nil" do
|
@@ -215,46 +216,22 @@ describe ::StashMagic do
|
|
215
216
|
lambda { @t.update(:instructions=>nil) }.should.not.raise
|
216
217
|
end
|
217
218
|
|
218
|
-
it "Should have ImageMagick string builder" do
|
219
|
-
@t = Treasure.create(:map=>@img)
|
220
|
-
|
221
|
-
@t.image_magick(:map, 'test.gif') do
|
222
|
-
im_write("-negate")
|
223
|
-
im_crop(200,100,20,10)
|
224
|
-
im_resize(nil, 100)
|
225
|
-
end.should=="-negate -crop 200x100+20+10 +repage -resize 'x100'"
|
226
|
-
F.exists?(@t.file_url(:map,'test.gif',true)).should==true
|
227
|
-
|
228
|
-
@t.image_magick(:map, 'test2.gif') do
|
229
|
-
im_write("-negate")
|
230
|
-
im_crop(200,100,20,10)
|
231
|
-
im_resize(nil, 100, '>')
|
232
|
-
end.should=="-negate -crop 200x100+20+10 +repage -resize 'x100>'"
|
233
|
-
F.exists?(@t.file_url(:map,'test2.gif',true)).should==true
|
234
|
-
|
235
|
-
@t.image_magick(:map, 'test3.gif') do
|
236
|
-
im_write("-negate")
|
237
|
-
im_crop(200,100,20,10)
|
238
|
-
im_resize(200, 100, '^')
|
239
|
-
end.should=="-negate -crop 200x100+20+10 +repage -resize '200x100^' -gravity center -extent 200x100"
|
240
|
-
F.exists?(@t.file_url(:map,'test3.gif',true)).should==true
|
241
|
-
|
242
|
-
@t.image_magick(:map, 'test4.gif') do
|
243
|
-
im_write("-negate")
|
244
|
-
im_crop(200,100,20,10)
|
245
|
-
im_resize(200, 100, '^', 'North')
|
246
|
-
end.should=="-negate -crop 200x100+20+10 +repage -resize '200x100^' -gravity North -extent 200x100"
|
247
|
-
F.exists?(@t.file_url(:map,'test4.gif',true)).should==true
|
248
|
-
end
|
249
|
-
|
250
219
|
it "Should be possible to overwrite the original image" do
|
251
220
|
@t = Treasure.create(:map=>@img)
|
252
|
-
url = @t.
|
221
|
+
url = @t.file_path(:map,nil,true)
|
253
222
|
size_before = F.size(url)
|
254
223
|
@t.convert(:map, '-resize 100x75')
|
255
224
|
F.size(url).should.not==size_before
|
256
225
|
end
|
257
226
|
|
227
|
+
it "Should be able to re-run all the after_stash in one method" do
|
228
|
+
@t = Treasure.create(:map=>@img)
|
229
|
+
url = @t.file_path(:map,'stash_thumb.gif',true)
|
230
|
+
time_before = F.mtime(url)
|
231
|
+
StashMagic.all_after_stash
|
232
|
+
F.mtime(url).should.not==time_before
|
233
|
+
end
|
234
|
+
|
258
235
|
::FileUtils.rm_rf(Treasure::PUBLIC) if F.exists?(Treasure::PUBLIC)
|
259
236
|
|
260
237
|
end
|
data/test/spec_s3.rb
ADDED
@@ -0,0 +1,258 @@
|
|
1
|
+
F = ::File
|
2
|
+
D = ::Dir
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'bacon'
|
6
|
+
|
7
|
+
require 'sequel'
|
8
|
+
DB = Sequel.sqlite
|
9
|
+
|
10
|
+
require 'tempfile'
|
11
|
+
|
12
|
+
$:.unshift(F.dirname(__FILE__)+'/../lib')
|
13
|
+
require 'stash_magic'
|
14
|
+
|
15
|
+
# S3 credentials
|
16
|
+
pseudo_env = File.join(F.dirname(__FILE__), '..', 'private', 'pseudo_env.rb')
|
17
|
+
load(pseudo_env) if File.exists?(pseudo_env)
|
18
|
+
AWS::S3::Base.establish_connection!(
|
19
|
+
:access_key_id => ENV['S3_KEY'],
|
20
|
+
:secret_access_key => ENV['S3_SECRET']
|
21
|
+
)
|
22
|
+
AWS::S3::Bucket.delete('campbellhay-stashmagictest', :force=>true)
|
23
|
+
AWS::S3::Bucket.create('campbellhay-stashmagictest')
|
24
|
+
|
25
|
+
class Treasure < ::Sequel::Model
|
26
|
+
BUCKET = 'campbellhay-stashmagictest'
|
27
|
+
::StashMagic.with_bucket(BUCKET)
|
28
|
+
|
29
|
+
plugin :schema
|
30
|
+
set_schema do
|
31
|
+
primary_key :id
|
32
|
+
Integer :age
|
33
|
+
String :map # jpeg
|
34
|
+
String :map_tooltip
|
35
|
+
String :map_alternative_text
|
36
|
+
String :mappy # jpeg - Used to see if mappy files are not destroyed when map is (because it starts the same)
|
37
|
+
String :instructions #pdf
|
38
|
+
end
|
39
|
+
create_table unless table_exists?
|
40
|
+
|
41
|
+
stash :map
|
42
|
+
stash :mappy
|
43
|
+
stash :instructions, :s3_store_options=>{:access=>:private}
|
44
|
+
|
45
|
+
def validate
|
46
|
+
errors[:age] << "Not old enough" unless (self.age.nil? || self.age>10)
|
47
|
+
errors[:instructions] << "Too big" if (!self.instructions.nil? && self.instructions[:size].to_i>46000)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class BadTreasure < ::Sequel::Model
|
52
|
+
include ::StashMagic
|
53
|
+
|
54
|
+
plugin :schema
|
55
|
+
set_schema do
|
56
|
+
primary_key :id
|
57
|
+
String :map # jpeg
|
58
|
+
String :instructions #pdf
|
59
|
+
end
|
60
|
+
create_table unless table_exists?
|
61
|
+
end
|
62
|
+
|
63
|
+
# Make temporary public folder
|
64
|
+
PUBLIC = F.expand_path(F.dirname(__FILE__)+'/public')
|
65
|
+
D.mkdir(PUBLIC) unless F.exists?(PUBLIC)
|
66
|
+
|
67
|
+
# =========
|
68
|
+
# = Tests =
|
69
|
+
# =========
|
70
|
+
|
71
|
+
describe 'StashMagic S3' do
|
72
|
+
|
73
|
+
`convert rose: #{PUBLIC}/rose.jpg` unless F.exists?(PUBLIC+'/rose.jpg') # Use ImageMagick to build a tmp image to use
|
74
|
+
`convert granite: #{PUBLIC}/granite.gif` unless F.exists?(PUBLIC+'/granite.gif') # Use ImageMagick to build a tmp image to use
|
75
|
+
`convert rose: #{PUBLIC}/rose.pdf` unless F.exists?(PUBLIC+'/rose.pdf') # Use ImageMagick to build a tmp image to use
|
76
|
+
`convert logo: #{PUBLIC}/logo.pdf` unless F.exists?(PUBLIC+'/logo.pdf')
|
77
|
+
|
78
|
+
def mock_upload(uploaded_file_path, content_type, binary=false)
|
79
|
+
n = F.basename(uploaded_file_path)
|
80
|
+
f = ::Tempfile.new(n)
|
81
|
+
f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
|
82
|
+
f.binmode if binary
|
83
|
+
::FileUtils.copy_file(uploaded_file_path, f.path)
|
84
|
+
{
|
85
|
+
:filename => n,
|
86
|
+
:type => content_type,
|
87
|
+
:tempfile => f
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
before do
|
92
|
+
@img = mock_upload(PUBLIC+'/rose.jpg', 'image/jpeg', true)
|
93
|
+
@img2 = mock_upload(PUBLIC+'/granite.gif', 'image/gif', true)
|
94
|
+
@pdf = mock_upload(PUBLIC+'/rose.pdf', 'application/pdf', true)
|
95
|
+
@pdf2 = mock_upload(PUBLIC+'/logo.pdf', 'application/pdf', true)
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'Should build S3 store options correctly' do
|
99
|
+
@t = Treasure.new
|
100
|
+
@t.s3_store_options(:map).should=={:access=>:public_read}
|
101
|
+
@t.s3_store_options(:instructions).should=={:access=>:private}
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'Should Include via Stash::with_bucket' do
|
105
|
+
Treasure.bucket.should==Treasure::BUCKET
|
106
|
+
end
|
107
|
+
|
108
|
+
it "Should stash entries with Class::stash and have reflection" do
|
109
|
+
Treasure.stash_reflection.keys.include?(:map).should==true
|
110
|
+
Treasure.stash_reflection.keys.include?(:instructions).should==true
|
111
|
+
end
|
112
|
+
|
113
|
+
it "Should give instance its own file_root" do
|
114
|
+
# Normal path
|
115
|
+
@t = Treasure.create
|
116
|
+
@t.file_root.should=="Treasure/#{@t.id}"
|
117
|
+
# Anonymous path
|
118
|
+
Treasure.new.file_root.should=='Treasure/tmp'
|
119
|
+
end
|
120
|
+
|
121
|
+
it "Should warn you when you have no storage chosen" do
|
122
|
+
lambda { Treasure.new.zzz }.should.raise(NoMethodError).message.should.not=='You have to choose a strorage system'
|
123
|
+
lambda { BadTreasure.new.bucket }.should.raise(NoMethodError).message.should=='You have to choose a strorage system'
|
124
|
+
end
|
125
|
+
|
126
|
+
it "Should not raise on setters eval when value already nil" do
|
127
|
+
Treasure.new.map.should==nil
|
128
|
+
end
|
129
|
+
|
130
|
+
it "Should have correct file_path values" do
|
131
|
+
# Original with no file - so we are not sure about extention
|
132
|
+
Treasure.new.file_path(:map).should==nil
|
133
|
+
# Original with file but not saved
|
134
|
+
Treasure.new(:map=>@img).file_path(:map).should=='Treasure/tmp/map.jpg'
|
135
|
+
# Style with file but not saved
|
136
|
+
Treasure.new(:map=>@img).file_path(:map, 'thumb.jpg').should=='Treasure/tmp/map.thumb.jpg' #not the right extention
|
137
|
+
end
|
138
|
+
|
139
|
+
it "Should have correct file_url values" do
|
140
|
+
# Original with no file - so we are not sure about extention
|
141
|
+
Treasure.new.file_url(:map).should==nil
|
142
|
+
# Original with file but not saved
|
143
|
+
Treasure.new(:map=>@img).file_url(:map).should=='http://s3.amazonaws.com/campbellhay-stashmagictest/Treasure/tmp/map.jpg'
|
144
|
+
# Same with SSL
|
145
|
+
Treasure.new(:map=>@img).file_url(:map, nil, true).should=='https://s3.amazonaws.com/campbellhay-stashmagictest/Treasure/tmp/map.jpg'
|
146
|
+
# Style with file but not saved
|
147
|
+
Treasure.new(:map=>@img).file_url(:map, 'thumb.jpg').should=='http://s3.amazonaws.com/campbellhay-stashmagictest/Treasure/tmp/map.thumb.jpg' #not the right extention
|
148
|
+
end
|
149
|
+
|
150
|
+
it "Should save the attachments when creating entry" do
|
151
|
+
@t = Treasure.create(:map => @img, :instructions => @pdf)
|
152
|
+
@t.map.should=={:name=>'map.jpg',:type=>'image/jpeg',:size=>2074}
|
153
|
+
AWS::S3::S3Object.exists?(@t.file_path(:map), Treasure.bucket).should==true
|
154
|
+
AWS::S3::S3Object.exists?(@t.file_path(:instructions), Treasure.bucket).should==true
|
155
|
+
AWS::S3::S3Object.exists?(@t.file_path(:map, 'stash_thumb.gif'), Treasure.bucket).should==true
|
156
|
+
AWS::S3::Bucket.objects(Treasure.bucket).map{|o|o.key}.member?(@t.file_path(:instructions, 'stash_thumb.gif')).should==false # https://github.com/marcel/aws-s3/issues/43
|
157
|
+
end
|
158
|
+
|
159
|
+
it "Should update attachment when updating entry" do
|
160
|
+
@t = Treasure.create(:map => @img).update(:map=>@img2)
|
161
|
+
@t.map.should=={:name=>'map.gif',:type=>'image/gif',:size=>7037}
|
162
|
+
AWS::S3::S3Object.exists?(@t.file_path(:map), Treasure.bucket).should==true
|
163
|
+
AWS::S3::S3Object.exists?(@t.file_path(:map).sub(/gif/, 'jpg'), Treasure.bucket).should==false
|
164
|
+
AWS::S3::S3Object.exists?(@t.file_path(:map, 'stash_thumb.gif'), Treasure.bucket).should==true
|
165
|
+
end
|
166
|
+
|
167
|
+
it "Should be able to remove attachments when column is set to nil" do
|
168
|
+
@t = Treasure.create(:map => @img, :mappy => @img2)
|
169
|
+
@t.map.should=={:name=>'map.jpg',:type=>'image/jpeg',:size=>2074}
|
170
|
+
@t.mappy.should=={:name=>'mappy.gif',:type=>'image/gif',:size=>7037}
|
171
|
+
AWS::S3::S3Object.exists?(@t.file_path(:map), Treasure.bucket).should==true
|
172
|
+
AWS::S3::S3Object.exists?(@t.file_path(:mappy), Treasure.bucket).should==true
|
173
|
+
AWS::S3::S3Object.exists?(@t.file_path(:map, 'stash_thumb.gif'), Treasure.bucket).should==true
|
174
|
+
@t.update(:map=>nil)
|
175
|
+
@t.map.should==nil
|
176
|
+
@t.mappy.should=={:name=>'mappy.gif',:type=>'image/gif',:size=>7037}
|
177
|
+
# AWS::S3::S3Object.exists?(@t.file_path(:map), Treasure.bucket).should==false
|
178
|
+
AWS::S3::Bucket.objects(Treasure.bucket).map{|o|o.key}.member?(@t.file_path(:map)).should==false # https://github.com/marcel/aws-s3/issues/43
|
179
|
+
AWS::S3::S3Object.exists?(@t.file_path(:mappy), Treasure.bucket).should==true
|
180
|
+
AWS::S3::Bucket.objects(Treasure.bucket).map{|o|o.key}.member?(@t.file_path(:map, 'stash_thumb.gif')).should==false # https://github.com/marcel/aws-s3/issues/43
|
181
|
+
# F.exists?(Treasure::PUBLIC+'/stash/Treasure/'+@t.id.to_s+'/map.stash_thumb.gif').should==false
|
182
|
+
end
|
183
|
+
|
184
|
+
it "Should have a function to retrieve the S3Object" do
|
185
|
+
t = Treasure.exclude(:map=>nil).first
|
186
|
+
obj = Treasure.new.s3object(:map, 'imaginary.gif')
|
187
|
+
obj.should==nil
|
188
|
+
|
189
|
+
lambda{ t.s3object(:map, 'imaginary.gif') }.should.raise(AWS::S3::NoSuchKey)
|
190
|
+
|
191
|
+
obj = t.s3object(:map)
|
192
|
+
obj.content_type.should=='image/jpeg'
|
193
|
+
end
|
194
|
+
|
195
|
+
it "Should be able to build image tags" do
|
196
|
+
@t = Treasure.create(:map => @img, :map_alternative_text => "Wonderful")
|
197
|
+
tag = @t.build_image_tag(:map)
|
198
|
+
tag.should.match(/^<img\s.+\s\/>$/)
|
199
|
+
tag.should.match(/\ssrc="http:\/\/s3.amazonaws.com\/campbellhay-stashmagictest\/Treasure\/#{@t.id}\/map.jpg"\s/)
|
200
|
+
tag.should.match(/\salt="Wonderful"\s/)
|
201
|
+
tag.should.match(/\stitle=""\s/)
|
202
|
+
end
|
203
|
+
|
204
|
+
it "Should be able to build image tags and override alt and title" do
|
205
|
+
@t = Treasure.create(:map => @img, :map_alternative_text => "Wonderful")
|
206
|
+
tag = @t.build_image_tag(:map,nil,:alt => 'Amazing & Beautiful Map')
|
207
|
+
tag.should.match(/^<img\s.+\s\/>$/)
|
208
|
+
tag.should.match(/\ssrc="http:\/\/s3.amazonaws.com\/campbellhay-stashmagictest\/Treasure\/#{@t.id}\/map.jpg"\s/)
|
209
|
+
tag.should.match(/\salt="Amazing & Beautiful Map"\s/)
|
210
|
+
tag.should.match(/\stitle=""\s/)
|
211
|
+
end
|
212
|
+
|
213
|
+
it "Should be able to handle validations" do
|
214
|
+
@t = Treasure.new(:instructions => @pdf2)
|
215
|
+
@t.valid?.should==false
|
216
|
+
AWS::S3::Bucket.objects(Treasure.bucket).map{|o|o.key}.member?(@t.file_path(:instructions)).should==false # https://github.com/marcel/aws-s3/issues/43
|
217
|
+
@t.set(:instructions => @pdf, :age => 8)
|
218
|
+
@t.valid?.should==false
|
219
|
+
AWS::S3::Bucket.objects(Treasure.bucket).map{|o|o.key}.member?(@t.file_path(:instructions)).should==false # https://github.com/marcel/aws-s3/issues/43
|
220
|
+
@t.set(:age => 12)
|
221
|
+
@t.valid?.should==true
|
222
|
+
@t.save
|
223
|
+
AWS::S3::S3Object.exists?(@t.file_path(:instructions), Treasure.bucket).should==true
|
224
|
+
end
|
225
|
+
|
226
|
+
it "Should not raise when updating the entry with blank string - which means the attachment is untouched" do
|
227
|
+
@t = Treasure.create(:instructions => @pdf)
|
228
|
+
before = @t.instructions
|
229
|
+
AWS::S3::S3Object.exists?(@t.file_path(:instructions), Treasure.bucket).should==true
|
230
|
+
@t.update(:instructions=>"")
|
231
|
+
@t.instructions.should==before
|
232
|
+
AWS::S3::S3Object.exists?(@t.file_path(:instructions), Treasure.bucket).should==true
|
233
|
+
end
|
234
|
+
|
235
|
+
it "Should not raise when the setter tries to destroy files when there is nothing to destroy" do
|
236
|
+
lambda { @t = Treasure.create(:instructions=>nil) }.should.not.raise
|
237
|
+
lambda { @t.update(:instructions=>nil) }.should.not.raise
|
238
|
+
end
|
239
|
+
|
240
|
+
it "Should be possible to overwrite the original image" do
|
241
|
+
@t = Treasure.create(:map=>@img)
|
242
|
+
url = @t.file_path(:map,nil)
|
243
|
+
size_before = AWS::S3::S3Object.find(url, Treasure.bucket).size
|
244
|
+
@t.convert(:map, '-resize 100x75')
|
245
|
+
AWS::S3::S3Object.find(url, Treasure.bucket).size.should.not==size_before
|
246
|
+
end
|
247
|
+
|
248
|
+
it "Should be able to re-run all the after_stash in one method" do
|
249
|
+
@t = Treasure.create(:map=>@img)
|
250
|
+
url = @t.file_path(:map,'stash_thumb.gif')
|
251
|
+
time_before = AWS::S3::S3Object.find(url, Treasure.bucket).about['last-modified']
|
252
|
+
StashMagic.all_after_stash
|
253
|
+
AWS::S3::S3Object.find(url, Treasure.bucket).about['last-modified'].should.not==time_before
|
254
|
+
end
|
255
|
+
|
256
|
+
::FileUtils.rm_rf(PUBLIC) if F.exists?(PUBLIC)
|
257
|
+
|
258
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stash-magic
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 27
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
+
- 1
|
8
9
|
- 0
|
9
|
-
|
10
|
-
version: 0.0.9
|
10
|
+
version: 0.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Mickael Riga
|
@@ -15,11 +15,11 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-
|
18
|
+
date: 2011-12-20 00:00:00 +00:00
|
19
19
|
default_executable:
|
20
20
|
dependencies: []
|
21
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.
|
22
|
+
description: A simple attachment system (file system or Amazon S3) that also handles thumbnails or other styles via ImageMagick. Originaly tested on Sequel ORM but purposedly easy to plug to something else.
|
23
23
|
email: mig@mypeplum.com
|
24
24
|
executables: []
|
25
25
|
|
@@ -31,9 +31,14 @@ files:
|
|
31
31
|
- .gitignore
|
32
32
|
- MIT_LICENCE
|
33
33
|
- README.rdoc
|
34
|
-
-
|
34
|
+
- lib/stash_magic.rb
|
35
|
+
- lib/stash_magic/image_magick_string_builder.rb
|
36
|
+
- lib/stash_magic/storage_filesystem.rb
|
37
|
+
- lib/stash_magic/storage_s3.rb
|
35
38
|
- stash_magic.gemspec
|
36
|
-
-
|
39
|
+
- test/spec_builder.rb
|
40
|
+
- test/spec_filesystem.rb
|
41
|
+
- test/spec_s3.rb
|
37
42
|
has_rdoc: true
|
38
43
|
homepage: http://github.com/mig-hub/stash_magic
|
39
44
|
licenses: []
|
@@ -42,7 +47,7 @@ post_install_message:
|
|
42
47
|
rdoc_options: []
|
43
48
|
|
44
49
|
require_paths:
|
45
|
-
-
|
50
|
+
- ./lib
|
46
51
|
required_ruby_version: !ruby/object:Gem::Requirement
|
47
52
|
none: false
|
48
53
|
requirements:
|
@@ -68,5 +73,5 @@ rubygems_version: 1.4.2
|
|
68
73
|
signing_key:
|
69
74
|
specification_version: 3
|
70
75
|
summary: File Attachment Made Simple
|
71
|
-
test_files:
|
72
|
-
|
76
|
+
test_files: []
|
77
|
+
|
data/stash_magic.rb
DELETED
@@ -1,198 +0,0 @@
|
|
1
|
-
require 'fileutils'
|
2
|
-
|
3
|
-
# A replacement for our current attachment system
|
4
|
-
# New requirements being:
|
5
|
-
# - More than one attachment per model
|
6
|
-
# - Easiest way to deal with folders (a bit like on our internal blog: the_wall)
|
7
|
-
# - Another way to deal with convert styles so that you can interract with it after saving the images (cropping for example)
|
8
|
-
# - Some facilities for pre-defined ImageMagick scripts
|
9
|
-
module StashMagic
|
10
|
-
|
11
|
-
F = ::File
|
12
|
-
D = ::Dir
|
13
|
-
FU = ::FileUtils
|
14
|
-
|
15
|
-
def self.included(into)
|
16
|
-
class << into
|
17
|
-
attr_reader :public_root
|
18
|
-
attr_accessor :stash_reflection
|
19
|
-
# Setter
|
20
|
-
def public_root=(location)
|
21
|
-
@public_root = location
|
22
|
-
FU.mkdir_p(location+'/stash/'+self.name.to_s)
|
23
|
-
end
|
24
|
-
# Declare a stash entry
|
25
|
-
def stash(name, options={})
|
26
|
-
stash_reflection.store name.to_sym, options
|
27
|
-
# Exemple of upload hash for attachments:
|
28
|
-
# { :type=>"image/jpeg",
|
29
|
-
# :filename=>"default.jpeg",
|
30
|
-
# :tempfile=>#<File:/var/folders/J0/J03dF6-7GCyxMhaB17F5yk+++TI/-Tmp-/RackMultipart.12704.0>,
|
31
|
-
# :head=>"Content-Disposition: form-data; name=\"model[attachment]\"; filename=\"default.jpeg\"\r\nContent-Type: image/jpeg\r\n",
|
32
|
-
# :name=>"model[attachment]"
|
33
|
-
# }
|
34
|
-
#
|
35
|
-
# SETTER
|
36
|
-
define_method name.to_s+'=' do |upload_hash|
|
37
|
-
return if upload_hash=="" # File in the form is unchanged
|
38
|
-
|
39
|
-
if upload_hash.nil?
|
40
|
-
destroy_files_for(name) unless self.__send__(name).nil?
|
41
|
-
super('')
|
42
|
-
else
|
43
|
-
|
44
|
-
@tempfile_path ||= {}
|
45
|
-
@tempfile_path[name.to_sym] = upload_hash[:tempfile].path
|
46
|
-
h = {
|
47
|
-
:name => name.to_s + upload_hash[:filename][/\.[^.]+$/],
|
48
|
-
:type => upload_hash[:type],
|
49
|
-
:size => upload_hash[:tempfile].size
|
50
|
-
}
|
51
|
-
super(h.inspect)
|
52
|
-
|
53
|
-
end
|
54
|
-
end
|
55
|
-
# GETTER
|
56
|
-
define_method name.to_s do |*args|
|
57
|
-
eval(super(*args).to_s)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
end
|
62
|
-
into.stash_reflection = {}
|
63
|
-
end
|
64
|
-
|
65
|
-
# Sugar
|
66
|
-
def public_root
|
67
|
-
self.class.public_root
|
68
|
-
end
|
69
|
-
|
70
|
-
# This method the path for images of a specific style(original by default)
|
71
|
-
# The argument 'full' means it returns the absolute path(used to save files)
|
72
|
-
# This could be a private method only used by file_url, but i keep it public just in case
|
73
|
-
def file_path(full=false)
|
74
|
-
raise "#{self.class}.public_root is not declared" if public_root.nil?
|
75
|
-
"#{public_root if full}/stash/#{self.class.to_s}/#{self.id || 'tmp'}"
|
76
|
-
end
|
77
|
-
|
78
|
-
# Returns the url of an attachment in a specific style(original if nil)
|
79
|
-
# The argument 'full' means it returns the absolute path(used to save files)
|
80
|
-
def file_url(attachment_name, style=nil, full=false)
|
81
|
-
f = __send__(attachment_name)
|
82
|
-
return nil if f.nil?
|
83
|
-
fn = style.nil? ? f[:name] : "#{attachment_name}.#{style}"
|
84
|
-
"#{file_path(full)}/#{fn}"
|
85
|
-
end
|
86
|
-
|
87
|
-
# Build the image tag with all SEO friendly info
|
88
|
-
# It's possible to add html attributes in a hash
|
89
|
-
def build_image_tag(attachment_name, style=nil, html_attributes={})
|
90
|
-
title_field, alt_field = (attachment_name.to_s+'_tooltip').to_sym, (attachment_name.to_s+'_alternative_text').to_sym
|
91
|
-
title = __send__(title_field) if columns.include?(title_field)
|
92
|
-
alt = __send__(alt_field) if columns.include?(alt_field)
|
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}="#{html_escape(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="", style=nil)
|
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=nil, &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 two 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, "-resize '100x75^' -gravity center -extent 100x75", 'stash_thumb.gif') 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
|
-
private
|
192
|
-
|
193
|
-
# Stolen from ERB
|
194
|
-
def html_escape(s)
|
195
|
-
s.to_s.gsub(/&/, "&").gsub(/\"/, """).gsub(/>/, ">").gsub(/</, "<")
|
196
|
-
end
|
197
|
-
|
198
|
-
end
|