model_attachment 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +23 -0
- data/Manifest.txt +12 -0
- data/README.rdoc +111 -0
- data/Rakefile +96 -0
- data/init.rb +1 -0
- data/lib/model_attachment.rb +305 -0
- data/lib/model_attachment/amazon.rb +143 -0
- data/lib/model_attachment/upfile.rb +49 -0
- data/test/assets/test.jpg +0 -0
- data/test/model_attachment_test.rb +197 -0
- metadata +95 -0
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
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
|
+
|