model_attachment 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ LICENSE
2
+
3
+ The MIT License
4
+
5
+ Copyright (c) 2010 Stephen Walker
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in
15
+ all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.
data/Manifest.txt ADDED
@@ -0,0 +1,12 @@
1
+ README.rdoc
2
+ LICENSE
3
+ Rakefile
4
+ init.rb
5
+ Manifest.txt
6
+ lib/model_attachment
7
+ lib/model_attachment/amazon.rb
8
+ lib/model_attachment/upfile.rb
9
+ lib/model_attachment.rb
10
+ test/assets
11
+ test/assets/test.jpg
12
+ test/model_attachment_test.rb
data/README.rdoc ADDED
@@ -0,0 +1,111 @@
1
+ =ModelAttachment
2
+
3
+ ModelAttachment is intended as an simple file attachment library for ActiveRecord. This is experimental software, the interface is subject to change and we are still adding tests. Use with caution only in development.
4
+
5
+ See the documentation for +has_attachement+ in ModelAttachment::ClassMethods for slightly more detailed options.
6
+
7
+ ==Quick Start
8
+
9
+ In your model:
10
+
11
+ class Document < ActiveRecord::Base
12
+ has_attachment :path => ":domain/:folder/:document/:version/",
13
+ :types => {
14
+ :small => { :command => 'convert -geometry 100x100' },
15
+ :large => { :command => 'convert -geometry 500x500' }
16
+ },
17
+ :aws => :default,
18
+ :logging => true
19
+
20
+ def domain
21
+ # returns string representing the domain
22
+ end
23
+
24
+ def folder
25
+ # returns string representing the folder
26
+ end
27
+
28
+ def document
29
+ # returns string representing document
30
+ end
31
+
32
+ def version
33
+ # returns document version number
34
+ end
35
+ end
36
+
37
+ In your migrations:
38
+
39
+ class AddAttachmentColumnsToDocument < ActiveRecord::Migration
40
+ def self.up
41
+ add_column :documents, :bucket, :string # if you are using aws
42
+ add_column :documents, :file_name, :string
43
+ add_column :documents, :content_type, :string
44
+ add_column :documents, :file_size, :integer
45
+ add_column :documents, :version, :integer
46
+ add_column :documents, :updated_at, :datetime
47
+ end
48
+
49
+ def self.down
50
+ remove_column :documents, :bucket
51
+ remove_column :documents, :file_name
52
+ remove_column :documents, :content_type
53
+ remove_column :documents, :file_size
54
+ remove_column :documents, :version
55
+ remove_column :documents, :updated_at
56
+ end
57
+ end
58
+
59
+ In your edit and new views:
60
+
61
+ <% form_for :document, @document, :url => document_path, :html => { :multipart => true } do |form| %>
62
+ <%= form.file_field :file_name %>
63
+ <% end %>
64
+
65
+ In your controller:
66
+
67
+ def create
68
+ @document = Document.create( params[:user] )
69
+ end
70
+
71
+ def send
72
+ # check permissions
73
+ @document = Document.find(params[:id])
74
+
75
+ # use x_send_file w/ apache for better results - http://github.com/simmerz/x_send_file
76
+ x_send_file(@document.full_filename(params[:type]), :type => @document.content_type, :disposition => 'inline')
77
+ end
78
+
79
+ In your show view:
80
+
81
+ <%= link_to "Download", @document.url(:large) %>
82
+
83
+ ==Usage
84
+
85
+ The basics of model_attachment are quite simple: Declare that your model has an attachment with the has_attachment method, and give it a name. ModelAttachment will wrap up up to four attributes and give the a friendly front end. The attributes are file_name, file_size, content_type, and updated_at.
86
+
87
+ Attachments can be validated with ModelAttachment's validation methods, validates_attachment_presence and validates_attachment_size.
88
+
89
+ Attachments can be moved to amazon with @document.move_to_amazon and from amazon with @document.move_to_filesystem.
90
+
91
+ You can use @document.url(:type) to get the url to the file.
92
+
93
+ ==Storage
94
+
95
+ The files that are assigned as attachments are, by default, placed in the directory specified by the :path option to has_attachment. By default, this location is ":rails_root/system/:document/:basename.:extention".
96
+
97
+ Options currently accepted and evaluated:
98
+ :domain
99
+ :folder
100
+ :document
101
+ :version
102
+
103
+ ==Post Processing
104
+
105
+ ModelAttachment supports post processing by sending the types a command, this will be run on any images.
106
+
107
+ ==Credit
108
+
109
+ This is a blatant strip down of the Paperclip module by Jon Yurek and thoughtbot, inc.
110
+
111
+
data/Rakefile ADDED
@@ -0,0 +1,96 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ $LOAD_PATH << File.join(File.dirname(__FILE__), 'lib')
6
+ require 'model_attachment'
7
+
8
+ desc 'Default: run unit tests.'
9
+ task :default => [:clean, :test]
10
+
11
+ desc 'Test the model_attachment plugin.'
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << 'lib' << 'profile'
14
+ t.pattern = 'test/**/*_test.rb'
15
+ t.verbose = true
16
+ end
17
+
18
+ desc 'Start an IRB session with all necessary files required.'
19
+ task :shell do |t|
20
+ chdir File.dirname(__FILE__)
21
+ exec 'irb -I lib/ -I lib/model_attachment -r rubygems -r active_record -r tempfile -r init'
22
+ end
23
+
24
+ desc 'Generate documentation for the model_attachment plugin.'
25
+ Rake::RDocTask.new(:rdoc) do |rdoc|
26
+ rdoc.rdoc_dir = 'doc'
27
+ rdoc.title = 'ModelAttachment'
28
+ rdoc.options << '--line-numbers' << '--inline-source'
29
+ rdoc.rdoc_files.include('README*')
30
+ rdoc.rdoc_files.include('lib/**/*.rb')
31
+ end
32
+
33
+ desc 'Update documentation on website'
34
+ task :sync_docs => 'rdoc' do
35
+ `rsync -ave ssh doc/ steve@rsweb2:/var/sites/stephenwalker/shared/system/docs/model_attachment`
36
+ end
37
+
38
+ desc 'Clean up files.'
39
+ task :clean do |t|
40
+ FileUtils.rm_rf "doc"
41
+ FileUtils.rm_rf "tmp"
42
+ FileUtils.rm_rf "pkg"
43
+ FileUtils.rm "test/debug.log" rescue nil
44
+ FileUtils.rm "test/model_attachment.db" rescue nil
45
+ Dir.glob("model_attachment-*.gem").each{|f| FileUtils.rm f }
46
+ end
47
+
48
+ include_file_globs = ["README*",
49
+ "LICENSE",
50
+ "Rakefile",
51
+ "init.rb",
52
+ "Manifest.txt",
53
+ "{generators,lib,tasks,test}/**/*"]
54
+
55
+ exclude_file_globs = ["test/amazon.yml",
56
+ "test/test.log"]
57
+
58
+ spec = Gem::Specification.new do |s|
59
+ s.name = "model_attachment"
60
+ s.description = "Simple file attachment for ActiveRecord models"
61
+ s.version = ModelAttachment::VERSION
62
+ s.author = "Steve Walker"
63
+ s.email = "steve@blackboxweb.com"
64
+ s.homepage = "http://github.com/stw/model_attachment"
65
+ s.platform = Gem::Platform::RUBY
66
+ s.summary = "Attach files to ActiveRecord models and run commands on images"
67
+ s.files = FileList[include_file_globs].to_a - FileList[exclude_file_globs].to_a
68
+ s.require_path = "lib"
69
+ s.test_files = FileList["test/**/test_*.rb"].to_a
70
+ s.rubyforge_project = "model_attachment"
71
+ s.has_rdoc = true
72
+ s.extra_rdoc_files = FileList["README*"].to_a
73
+ s.rdoc_options << '--line-numbers' << '--inline-source'
74
+ s.requirements << "ImageMagick"
75
+ s.add_development_dependency 'sqlite3-ruby'
76
+ s.add_development_dependency 'activerecord'
77
+ end
78
+
79
+ desc "Print a list of the files to be put into the gem"
80
+ task :manifest => :clean do
81
+ spec.files.each do |file|
82
+ puts file
83
+ end
84
+ end
85
+
86
+ desc "Generate a gemspec file for GitHub"
87
+ task :gemspec => :clean do
88
+ File.open("#{spec.name}.gemspec", 'w') do |f|
89
+ f.write spec.to_ruby
90
+ end
91
+ end
92
+
93
+ desc "Build the gem into the current directory"
94
+ task :gem => :gemspec do
95
+ `gem build #{spec.name}.gemspec`
96
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), "lib", "model_attachment")
@@ -0,0 +1,305 @@
1
+ #
2
+ # model_attachment.rb
3
+ #
4
+ # Created by Stephen Walker on 2010-04-22.
5
+ # Copyright 2010 Stephen Walker. All rights reserved.
6
+ #
7
+
8
+ # TODO - Add exception handling and validation testing
9
+
10
+ require 'rubygems'
11
+ require 'yaml'
12
+ require 'model_attachment/upfile'
13
+ require 'model_attachment/amazon'
14
+
15
+ # The base module that gets included in ActiveRecord::Base.
16
+ module ModelAttachment
17
+ VERSION = "0.0.5"
18
+
19
+ class << self
20
+
21
+ def included base #:nodoc:
22
+ base.extend ClassMethods
23
+ end
24
+
25
+ end
26
+
27
+ module ClassMethods
28
+ # +has_attachment+ adds the ability to upload files and make thumbnails of images
29
+ # * +path+: the path format for saving the documents
30
+ # * +types+: a hash of thumbnails to create for images
31
+ # :types => {
32
+ # :small => { :command => 'convert -geometry 100x100' },
33
+ # :large => { :command => 'convert -geometry 500x500' }
34
+ # }
35
+ # * +aws+: the path to the aws config file or :default for rails config/amazon.yml
36
+ # access_key_id:
37
+ # secret_access_key:
38
+ # * +logging+: set to true to see logging
39
+ def has_attachment(options = {})
40
+ include InstanceMethods
41
+
42
+ if options[:aws]
43
+ begin
44
+ require 'aws/s3'
45
+ rescue LoadError => e
46
+ e.messages << "You man need to install the aws-s3 gem"
47
+ raise e
48
+ end
49
+ end
50
+
51
+ if options[:aws] == :default
52
+ config_file = File.join(RAILS_ROOT, "config", "amazon.yml")
53
+ if File.exist?(config_file)
54
+ options[:aws] = config_file
55
+ include AmazonInstanceMethods
56
+ else
57
+ raise("You must provide a config/amazon.yml setup file")
58
+ end
59
+ elsif !options[:aws].nil? && File.exist?(options[:aws])
60
+ include AmazonInstanceMethods
61
+ end
62
+
63
+ write_inheritable_attribute(:attachment_options, options)
64
+
65
+ # must be before the save to save the attributes
66
+ before_save :save_attributes
67
+
68
+ # must be after save to get the id for the path
69
+ after_save :save_attached_files
70
+ before_destroy :destroy_attached_files
71
+
72
+ end
73
+
74
+ # Places ActiveRecord-style validations on the size of the file assigned. The
75
+ # possible options are:
76
+ # * +in+: a Range of bytes (i.e. +1..1.megabyte+),
77
+ # * +less_than+: equivalent to :in => 0..options[:less_than]
78
+ # * +greater_than+: equivalent to :in => options[:greater_than]..Infinity
79
+ # * +message+: error message to display, use :min and :max as replacements
80
+ # * +if+: A lambda or name of a method on the instance. Validation will only
81
+ # be run is this lambda or method returns true.
82
+ # * +unless+: Same as +if+ but validates if lambda or method returns false.
83
+ def validates_attachment_size name, options = {}
84
+ min = options[:greater_than] || (options[:in] && options[:in].first) || 0
85
+ max = options[:less_than] || (options[:in] && options[:in].last) || (1.0/0)
86
+ range = (min..max)
87
+ message = options[:message] || "file size must be between :min and :max bytes."
88
+ message = message.gsub(/:min/, min.to_s).gsub(/:max/, max.to_s)
89
+
90
+ validates_inclusion_of name,
91
+ :in => range,
92
+ :message => message,
93
+ :if => options[:if],
94
+ :unless => options[:unless]
95
+ end
96
+
97
+ # Places ActiveRecord-style validations on the presence of a file.
98
+ # Options:
99
+ # * +if+: A lambda or name of a method on the instance. Validation will only
100
+ # be run is this lambda or method returns true.
101
+ # * +unless+: Same as +if+ but validates if lambda or method returns false.
102
+ def validates_attachment_presence name, options = {}
103
+ message = options[:message] || "must be set."
104
+ validates_presence_of name,
105
+ :message => message,
106
+ :if => options[:if],
107
+ :unless => options[:unless]
108
+ end
109
+
110
+ # Returns attachment options defined by each call to acts_as_attachment.
111
+ def attachment_options
112
+ read_inheritable_attribute(:attachment_options)
113
+ end
114
+
115
+ end
116
+
117
+ module InstanceMethods #:nodoc:
118
+
119
+ # return the url based on location
120
+ # * +proto+: set the protocol, defaults to http
121
+ # * +port+: sets the port if required
122
+ # * +server_name+: sets the server name, defaults to localhost
123
+ # * +path+: sets the path, defaults to /documents/send
124
+ # * +type+: sets the type, types come from the has_attachment method, ex. small, large
125
+ def url(options = {})
126
+ proto = options[:proto] || "http"
127
+ port = options[:port]
128
+ server_name = options[:server_name] || "localhost"
129
+ url_path = options[:path] || "/documents/send"
130
+ type = options[:type]
131
+ server_name += ":" + port if port
132
+
133
+ url = (bucket.nil? ? "#{proto}://#{server_name}#{url_path}?id=#{id}" : aws_url(type))
134
+ log("Providing URL: #{url}")
135
+ return url
136
+ end
137
+
138
+ # returns the rails path of the file
139
+ def path
140
+ if (self.class.attachment_options[:path])
141
+ return "/system" + interpolate(self.class.attachment_options[:path])
142
+ else
143
+ return "/system/" + sprintf("%04d", id) + "/"
144
+ end
145
+ end
146
+
147
+ # returns the full system path of the file
148
+ def full_path
149
+ RAILS_ROOT + path
150
+ end
151
+
152
+ # returns the filename, including any type modifier
153
+ # +type+: type from has_attachment, ex. small, large
154
+ def filename(type = "")
155
+ type = "_#{type}" if type != ""
156
+ "#{basename}#{type}#{extension}"
157
+ end
158
+
159
+ def extension #:nodoc:
160
+ File.extname(file_name)
161
+ end
162
+
163
+ def basename #:nodoc:
164
+ File.basename(file_name, extension)
165
+ end
166
+
167
+ # returns the full system path/filename
168
+ # +type+: type from has_attachment, ex. small, large
169
+ def full_filename(type = "")
170
+ full_path + filename(type)
171
+ end
172
+
173
+ # decide whether or not this is an image
174
+ def image?
175
+ content_type =~ /^image\//
176
+ end
177
+
178
+ private
179
+
180
+ def process_image_types #:nodoc:
181
+ if self.class.attachment_options[:types]
182
+ self.class.attachment_options[:types].each do |name, value|
183
+ if image?
184
+ yield(name, value)
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ # save the correct attribute info before the save
191
+ def save_attributes
192
+ return if file_name.class.to_s == "String"
193
+ @temp_file = self.file_name
194
+
195
+ # get original filename info and clean up for storage
196
+ ext = File.extname(@temp_file.original_filename)
197
+ base = File.basename(@temp_file.original_filename, ext).strip.gsub(/[^A-Za-z\d\.\-_]+/, '_')
198
+
199
+ # save attributes
200
+ self.file_name = base + ext
201
+ self.content_type = @temp_file.content_type.strip
202
+ self.file_size = @temp_file.size.to_i
203
+ self.updated_at = Time.now
204
+
205
+ end
206
+
207
+ # Does all the file processing, moves from temp, processes images
208
+ def save_attached_files
209
+ return if @temp_file.nil? or @temp_file == ""
210
+ options = self.class.attachment_options
211
+
212
+ log("Path: #{path} Basename: #{basename} Extension: #{extension}")
213
+
214
+ # copy image to correct path
215
+ FileUtils.mkdir_p(full_path)
216
+ FileUtils.chmod(0755, full_path)
217
+ FileUtils.mv(@temp_file.path, full_path + basename + extension)
218
+
219
+ # run any processing passed in on images
220
+ process_images
221
+
222
+ @dirty = true
223
+ @temp_file.close if @temp_file.respond_to?(:close)
224
+ @temp_file = nil
225
+ end
226
+
227
+ # run each processor on file
228
+ def process_images
229
+ process_image_types do |name, value|
230
+ command = value[:command]
231
+ old_filename = full_filename
232
+ new_filename = full_filename(name)
233
+ log("Create #{name} by running #{command} on #{old_filename}")
234
+ log("Created: #{new_filename}")
235
+ `#{command} #{old_filename} #{new_filename}`
236
+ end
237
+ end
238
+
239
+ # create the path based on the template
240
+ def interpolate(path, *args)
241
+ methods = ["domain", "folder", "document", "version"]
242
+ methods.reverse.inject( path.dup ) do |result, tag|
243
+ result.gsub(/:#{tag}/) do |match|
244
+ send( tag, *args )
245
+ end
246
+ end
247
+ end
248
+
249
+ # removes any files associated with this instance
250
+ def destroy_attached_files
251
+ begin
252
+
253
+ if bucket.nil?
254
+ log("Deleting #{full_filename}")
255
+ FileUtils.rm(full_filename) if File.exist?(full_filename)
256
+
257
+ # delete thumbnails if image
258
+ process_image_types do |name, value|
259
+ log("Deleting #{name}")
260
+ FileUtils.rm(full_filename(name)) if File.exists?(full_filename(name))
261
+ end
262
+
263
+ else
264
+ remove_from_amazon
265
+ end
266
+
267
+ rescue Errno::ENOENT => e
268
+ # ignore file-not-found, let everything else pass
269
+ end
270
+ begin
271
+ while(true)
272
+ dir_path = File.dirname(full_filename)
273
+ FileUtils.rmdir(dir_path)
274
+ end
275
+ rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR
276
+ # Stop trying to remove parent directories
277
+ rescue SystemCallError => e
278
+ log("There was an unexpected error while deleting directories: #{e.class}")
279
+ # Ignore it
280
+ end
281
+ end
282
+
283
+ def logging? #:nodoc:
284
+ self.class.attachment_options[:logging]
285
+ end
286
+
287
+ # Log a ModelAttachment specific message
288
+ # +message+: message to be logged if logging? true
289
+ def log(message)
290
+ logger.info("[model_attachment] #{message}") if logging?
291
+ end
292
+
293
+ def dirty? #:nodoc:
294
+ @dirty
295
+ end
296
+
297
+ end
298
+
299
+ end
300
+
301
+ # Set it up in our model
302
+ if Object.const_defined?("ActiveRecord")
303
+ ActiveRecord::Base.send(:include, ModelAttachment)
304
+ File.send(:include, ModelAttachment::Upfile)
305
+ end
@@ -0,0 +1,143 @@
1
+ module ModelAttachment
2
+ module AmazonInstanceMethods
3
+
4
+ # returns the aws url
5
+ # +type+: type passed to has_attachment, ex. small, large
6
+ def aws_url(type = "")
7
+ begin
8
+ return AWS::S3::S3Object.find(aws_key(type), default_bucket).url
9
+ rescue
10
+ log("Could not get object: #{aws_key(type)}")
11
+ end
12
+ end
13
+
14
+ # sets the default aws bucket
15
+ # +current_bucket+: set the current bucket, default 'globalfolders'
16
+ def default_bucket(current_bucket = 'globalfolders')
17
+ current_bucket
18
+ end
19
+
20
+ # creates the aws_key
21
+ # +type+: type passed to has_attachment, ex. small, large
22
+ def aws_key(type = "")
23
+ file = (type.nil? || type == "" ? filename : basename + "_" + type + extension)
24
+ (path + file).gsub!(/^\//,'')
25
+ end
26
+
27
+ # connect to aws, uses access_key_id and secret_access_key in config/amazon.yml
28
+ def aws_connect
29
+ return if AWS::S3::Base.connected?
30
+
31
+ begin
32
+ config = YAML.load_file(self.class.attachment_options[:aws])
33
+ if config
34
+ log("Connect to Amazon")
35
+ AWS::S3::Base.establish_connection!(
36
+ :access_key_id => config['access_key_id'],
37
+ :secret_access_key => config['secret_access_key'],
38
+ :use_ssl => true,
39
+ :persistent => true # if issues with disconnections, set to false
40
+ )
41
+ else
42
+ raise "You must provide an amazon.yml config file"
43
+ end
44
+ rescue AWS::S3::ResponseError => error
45
+ log("Could not connect to amazon: #{error.message}")
46
+ end
47
+ end
48
+
49
+ # moves file to amazon along with modified images, removes local images once object existence is confirmed
50
+ def move_to_amazon
51
+ aws_connect
52
+
53
+ log("Move #{aws_key} to Amazon.")
54
+ begin
55
+ AWS::S3::S3Object.store(aws_key, open(full_filename), default_bucket, :content_type => content_type)
56
+
57
+ # copy over modified files
58
+ process_image_types do |name, value|
59
+ AWS::S3::S3Object.store(aws_key(name.to_s), open(full_filename), default_bucket, :content_type => content_type)
60
+ end
61
+
62
+ self.bucket = default_bucket
63
+ @dirty = true
64
+ save!
65
+ rescue AWS::S3::ResponseError => error
66
+ log("Store Object Failed: #{error.message}")
67
+ rescue StandardError => e
68
+ log("Move to Amazon Failed: #{e.message}")
69
+ end
70
+
71
+ begin
72
+ if AWS::S3::S3Object.exists?(aws_key, default_bucket)
73
+ log("Remove Filename #{full_path + filename}")
74
+ FileUtils.rm(full_path + filename)
75
+ end
76
+
77
+ # remove any modified local files
78
+ process_image_types do |name, value|
79
+ if AWS::S3::S3Object.exists?(aws_key(name.to_s), default_bucket)
80
+ log("Remove Filename #{full_path + filename(name)}")
81
+ FileUtils.rm(full_path + filename(name.to_s))
82
+ end
83
+ end
84
+ rescue AWS::S3::ResponseError => error
85
+ log("Could not check objects existence: #{error.message}")
86
+ rescue StandardError => error
87
+ log("Removing file failed: #{error.message}.")
88
+ end
89
+ end
90
+
91
+ # moves files back to local filesystem, along with modified images, removes from amazon once they are confirmed locally
92
+ def move_to_filesystem
93
+ aws_connect
94
+ begin
95
+ open(full_filename, 'w') do |file|
96
+ AWS::S3::S3Object.stream(path + file_name, default_bucket) do |chunk|
97
+ file.write chunk
98
+ end
99
+ end
100
+
101
+ # copy over modified files
102
+ process_image_types do |name, value|
103
+ open(full_filename(name), 'w') do |file|
104
+ AWS::S3::S3Object.stream(path + filename(name), default_bucket) do |chunk|
105
+ file.write chunk
106
+ end
107
+ end
108
+ end
109
+
110
+ rescue AWS::S3::ResponseError => error
111
+ log("Copying File to local filesystem failed: #{error.message}")
112
+ end
113
+
114
+ if File.size(full_filename) == file_size
115
+ remove_from_amazon
116
+ end
117
+ end
118
+
119
+ # removes files from amazon
120
+ def remove_from_amazon
121
+ begin
122
+ log("Removing #{aws_key} from Amazon")
123
+ object = AWS::S3::S3Object.find(aws_key, default_bucket)
124
+ object.delete
125
+
126
+ # remove modified files
127
+ process_image_types do |name, value|
128
+ AWS::S3::S3Object.find(aws_key(name.to_s), default_bucket).delete
129
+ end
130
+
131
+ # make sure we set the bucket to nil so we know they're local
132
+ self.bucket = nil
133
+ @dirty = true
134
+ save!
135
+ rescue AWS::S3::ResponseError => error
136
+ log("Removing file from amazon failed: #{error.message}")
137
+ rescue StandardError => e
138
+ log("Failed remove from Amazon: #{e.message}")
139
+ end
140
+ end
141
+
142
+ end
143
+ end
@@ -0,0 +1,49 @@
1
+ module ModelAttachment
2
+ # The Upfile module is a convenience module for adding uploaded-file-type methods
3
+ # to the +File+ class. Useful for testing.
4
+ # user.avatar = File.new("test/test_avatar.jpg")
5
+ module Upfile
6
+
7
+ # Infer the MIME-type of the file from the extension.
8
+ def content_type
9
+ type = (self.path.match(/\.(\w+)$/)[1] rescue "octet-stream").downcase
10
+ case type
11
+ when %r"jp(e|g|eg)" then "image/jpeg"
12
+ when %r"tiff?" then "image/tiff"
13
+ when %r"png", "gif", "bmp" then "image/#{type}"
14
+ when "txt" then "text/plain"
15
+ when %r"html?" then "text/html"
16
+ when "js" then "application/js"
17
+ when "csv", "xml", "css" then "text/#{type}"
18
+ else
19
+ Paperclip.run("file", "--mime-type #{self.path}").split(':').last.strip rescue "application/x-#{type}"
20
+ end
21
+ end
22
+
23
+ # Returns the file's normal name.
24
+ def original_filename
25
+ File.basename(self.path)
26
+ end
27
+
28
+ # Returns the size of the file.
29
+ def size
30
+ File.size(self)
31
+ end
32
+ end
33
+ end
34
+
35
+ if defined? StringIO
36
+ class StringIO
37
+ attr_accessor :original_filename, :content_type
38
+ def original_filename
39
+ @original_filename ||= "stringio.txt"
40
+ end
41
+ def content_type
42
+ @content_type ||= "text/plain"
43
+ end
44
+ end
45
+ end
46
+
47
+ class File #:nodoc:
48
+ include ModelAttachment::Upfile
49
+ end
Binary file
@@ -0,0 +1,197 @@
1
+ #
2
+ # model_attachment_test.rb
3
+ #
4
+ # Created by Stephen Walker on 2010-04-22.
5
+ # Copyright 2010 Stephen Walker. All rights reserved.
6
+ #
7
+
8
+ require 'test/unit'
9
+
10
+ require 'rubygems'
11
+ require 'active_record'
12
+ require 'fileutils'
13
+
14
+ $:.unshift File.dirname(__FILE__) + '/../lib'
15
+ require File.dirname(__FILE__) + '/../init'
16
+
17
+ RAILS_ROOT = File.dirname(__FILE__)
18
+
19
+ class Test::Unit::TestCase
20
+ end
21
+
22
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
23
+
24
+ # keep AR from printing schema statements
25
+ $stdout = StringIO.new
26
+
27
+ def setup_db
28
+ #FileUtils.rm(RAILS_ROOT + "/test.log")
29
+ ActiveRecord::Base.logger = Logger.new(RAILS_ROOT + "/test.log")
30
+
31
+ ActiveRecord::Schema.define(:version => 1) do
32
+ create_table :documents do |t|
33
+ t.string :name
34
+ t.string :bucket
35
+ t.string :file_name
36
+ t.string :content_type
37
+ t.integer :file_size
38
+ t.timestamps
39
+ end
40
+ end
41
+ end
42
+
43
+ def teardown_db
44
+ ActiveRecord::Base.connection.tables.each do |t|
45
+ ActiveRecord::Base.connection.drop_table(t)
46
+ end
47
+ end
48
+
49
+ class Document < ActiveRecord::Base
50
+ def domain
51
+ "bbs"
52
+ end
53
+
54
+ def folder
55
+ "1"
56
+ end
57
+
58
+ def document
59
+ "1"
60
+ end
61
+ def version
62
+ "0"
63
+ end
64
+ end
65
+
66
+ class DocumentDefault < Document
67
+ has_attachment
68
+ end
69
+
70
+ class DocumentNoResize < Document
71
+ has_attachment :path => "/:domain/:folder/:document/:version/"
72
+ end
73
+
74
+ class DocumentWithResize < Document
75
+ has_attachment :path => "/:domain/:folder/:document/:version/",
76
+ :types => {
77
+ :small => { :command => '/opt/local/bin/convert -geometry 100x100' }
78
+ }
79
+ end
80
+
81
+ class DocumentWithAWS < Document
82
+ has_attachment :path => "/:domain/:folder/:document/:version/",
83
+ :aws => File.join(File.dirname(__FILE__), "amazon.yml"),
84
+ :types => {
85
+ :small => { :command => '/opt/local/bin/convert -geometry 100x100' }
86
+ },
87
+ :logging => true
88
+ end
89
+
90
+ class ModelAttachmentTest < Test::Unit::TestCase
91
+
92
+ def setup
93
+ setup_db
94
+ end
95
+
96
+ def teardown
97
+ teardown_db
98
+ end
99
+
100
+ def test_creation
101
+ document = Document.new
102
+ assert_equal document.class.to_s, "Document"
103
+ end
104
+
105
+ def test_no_resize_creation
106
+ document = DocumentNoResize.new
107
+ assert_equal document.class.to_s, "DocumentNoResize"
108
+ end
109
+
110
+ def test_with_resize
111
+ document = DocumentWithResize.new
112
+ assert_equal document.class.to_s, "DocumentWithResize"
113
+ end
114
+
115
+ def test_save_default
116
+ FileUtils.cp(RAILS_ROOT + "/assets/test.jpg", RAILS_ROOT + "/assets/test1.jpg")
117
+ file = File.open(RAILS_ROOT + "/assets/test1.jpg")
118
+
119
+ document = DocumentDefault.new(:name => "Test", :file_name => file)
120
+ document.save
121
+
122
+ assert_equal "test1.jpg", document.file_name
123
+ assert_equal "image/jpeg", document.content_type
124
+
125
+ assert File.exists?(RAILS_ROOT + "/system/0001/test1.jpg")
126
+
127
+ document.destroy
128
+ assert !File.exists?(RAILS_ROOT + "/system/0001/test1.jpg")
129
+ end
130
+
131
+ def test_save_with_no_resize
132
+ FileUtils.cp(RAILS_ROOT + "/assets/test.jpg", RAILS_ROOT + "/assets/test1.jpg")
133
+ file = File.open(RAILS_ROOT + "/assets/test1.jpg")
134
+
135
+ document = DocumentNoResize.new(:name => "Test", :file_name => file)
136
+ document.save
137
+
138
+ assert_equal document.file_name, "test1.jpg"
139
+ assert_equal document.content_type, "image/jpeg"
140
+
141
+ assert File.exists?(RAILS_ROOT + "/system/bbs/1/1/0/test1.jpg")
142
+
143
+ document.destroy
144
+ assert !File.exists?(RAILS_ROOT + "/system/bbs/1/1/0/test1.jpg")
145
+ end
146
+
147
+ def test_save_with_resize
148
+ FileUtils.cp(RAILS_ROOT + "/assets/test.jpg", RAILS_ROOT + "/assets/test2.jpg")
149
+ file = File.open(RAILS_ROOT + "/assets/test2.jpg")
150
+
151
+ document = DocumentWithResize.new(:name => "Test", :file_name => file)
152
+ document.save
153
+
154
+ assert_equal document.file_name, "test2.jpg"
155
+ assert_equal document.content_type, "image/jpeg"
156
+
157
+ assert File.exists?(RAILS_ROOT + "/system/bbs/1/1/0/test2.jpg")
158
+ assert File.exists?(RAILS_ROOT + "/system/bbs/1/1/0/test2_small.jpg")
159
+
160
+ document.destroy
161
+ assert !File.exists?(RAILS_ROOT + "/system/bbs/1/1/0/test2.jpg")
162
+ assert !File.exists?(RAILS_ROOT + "/system/bbs/1/1/0/test2_small.jpg")
163
+ end
164
+
165
+ def test_save_with_aws
166
+ FileUtils.cp(RAILS_ROOT + "/assets/test.jpg", RAILS_ROOT + "/assets/test3.jpg")
167
+ file = File.open(RAILS_ROOT + "/assets/test3.jpg")
168
+
169
+ document = DocumentWithAWS.new(:name => "Test", :file_name => file)
170
+ document.save
171
+ assert File.exist?(RAILS_ROOT + "/system/bbs/1/1/0/test3.jpg")
172
+
173
+ assert_equal "http://localhost:3000/documents/send?id=1", document.url(:port => "3000")
174
+ assert_equal "test3.jpg", document.file_name
175
+ assert_equal String, document.file_name.class
176
+ assert_equal "image/jpeg", document.content_type
177
+
178
+ document.move_to_amazon
179
+
180
+ assert_match /https:\/\/s3.amazonaws.com\/globalfolders\/system\/bbs\/1\/1\/0\/test3\.jpg/, document.url
181
+ assert_match /https:\/\/s3.amazonaws.com\/globalfolders\/system\/bbs\/1\/1\/0\/test3_small\.jpg/, document.url(:type => "small")
182
+
183
+ assert_equal 'globalfolders', document.bucket
184
+ assert !File.exists?(RAILS_ROOT + "/system/bbs/1/1/0/test3.jpg")
185
+ assert !File.exists?(RAILS_ROOT + "/system/bbs/1/1/0/test3_small.jpg")
186
+
187
+ document.move_to_filesystem
188
+
189
+ assert File.exists?(RAILS_ROOT + "/system/bbs/1/1/0/test3.jpg")
190
+ assert File.exists?(RAILS_ROOT + "/system/bbs/1/1/0/test3_small.jpg")
191
+
192
+ document.destroy
193
+ assert !File.exists?(RAILS_ROOT + "/system/bbs/1/1/0/test3.jpg")
194
+ assert !File.exists?(RAILS_ROOT + "/system/bbs/1/1/0/test3_small.jpg")
195
+ end
196
+ end
197
+
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: model_attachment
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 5
9
+ version: 0.0.5
10
+ platform: ruby
11
+ authors:
12
+ - Steve Walker
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-04-26 00:00:00 -04:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: sqlite3-ruby
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :development
31
+ version_requirements: *id001
32
+ - !ruby/object:Gem::Dependency
33
+ name: activerecord
34
+ prerelease: false
35
+ requirement: &id002 !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ segments:
40
+ - 0
41
+ version: "0"
42
+ type: :development
43
+ version_requirements: *id002
44
+ description: Simple file attachment for ActiveRecord models
45
+ email: steve@blackboxweb.com
46
+ executables: []
47
+
48
+ extensions: []
49
+
50
+ extra_rdoc_files:
51
+ - README.rdoc
52
+ files:
53
+ - README.rdoc
54
+ - LICENSE
55
+ - Rakefile
56
+ - init.rb
57
+ - Manifest.txt
58
+ - lib/model_attachment/amazon.rb
59
+ - lib/model_attachment/upfile.rb
60
+ - lib/model_attachment.rb
61
+ - test/assets/test.jpg
62
+ - test/model_attachment_test.rb
63
+ has_rdoc: true
64
+ homepage: http://github.com/stw/model_attachment
65
+ licenses: []
66
+
67
+ post_install_message:
68
+ rdoc_options:
69
+ - --line-numbers
70
+ - --inline-source
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ segments:
78
+ - 0
79
+ version: "0"
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ segments:
85
+ - 0
86
+ version: "0"
87
+ requirements:
88
+ - ImageMagick
89
+ rubyforge_project: model_attachment
90
+ rubygems_version: 1.3.6
91
+ signing_key:
92
+ specification_version: 3
93
+ summary: Attach files to ActiveRecord models and run commands on images
94
+ test_files: []
95
+