stash-magic 0.0.9 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|