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 CHANGED
@@ -1 +1,3 @@
1
1
  pkg/*
2
+ private/*
3
+ *.swp
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 attachments in a database and help you with thumbnails or other styles via ImageMagick (hence, the name). Features are:
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 (one file) module
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 do both in one line though:
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 is not used by the module itself, but could be handy for you as you can have it in the stash reflection:
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 third argument which is a boolean. When set to true, it will give you the absolute path to the file:
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
- @treasure_instance.file_url(:map, nil, true) # /absolute/path/to/public/stash/Treasure/1/map.pdf
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 couragous amongst you, here is the way you create a very simple style for the portrait attachment:
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 automaticaly for every image a thumbnail called 'stash_thumb.gif'.
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.14.0
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
@@ -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(/&/, "&amp;").gsub(/\"/, "&quot;").gsub(/>/, "&gt;").gsub(/</, "&lt;")
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.9"
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.test_files = ['spec.rb']
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
- require F.dirname(__FILE__)+'/stash_magic'
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 ::StashMagic do
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 file_path" do
104
+ it "Should give instance its own file_root" do
105
105
  # Normal path
106
106
  @t = Treasure.create
107
- @t.file_path.should=="/stash/Treasure/#{@t.id}"
107
+ @t.file_root.should=="/stash/Treasure/#{@t.id}"
108
108
  # Anonymous path
109
- Treasure.new.file_path.should=='/stash/Treasure/tmp'
109
+ Treasure.new.file_root.should=='/stash/Treasure/tmp'
110
110
  # Normal path full
111
111
  @t = Treasure.create
112
- @t.file_path(true).should==Treasure::PUBLIC+"/stash/Treasure/#{@t.id}"
112
+ @t.file_root(true).should==Treasure::PUBLIC+"/stash/Treasure/#{@t.id}"
113
113
  # Anonymous path full
114
- Treasure.new.file_path(true).should==Treasure::PUBLIC+'/stash/Treasure/tmp'
114
+ Treasure.new.file_root(true).should==Treasure::PUBLIC+'/stash/Treasure/tmp'
115
115
  end
116
116
 
117
- it "Should always raise on file_path if public_root is not declared" do
118
- lambda { BadTreasure.new.file_path }.should.raise(RuntimeError).message.should=='BadTreasure.public_root is not declared'
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.file_url(:map,nil,true)
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 &amp; 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: 13
4
+ hash: 27
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
+ - 1
8
9
  - 0
9
- - 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-07-05 00:00:00 +01:00
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
- - spec.rb
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
- - stash_magic.rb
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
- - spec.rb
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(/&/, "&amp;").gsub(/\"/, "&quot;").gsub(/>/, "&gt;").gsub(/</, "&lt;")
196
- end
197
-
198
- end