ss-attachment_fu 3.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +99 -0
- data/LICENSE +20 -0
- data/README.rdoc +358 -0
- data/amazon_s3.yml.tpl +17 -0
- data/lib/geometry.rb +96 -0
- data/lib/pothoven-attachment_fu.rb +27 -0
- data/lib/technoweenie/attachment_fu/backends/cloud_file_backend.rb +212 -0
- data/lib/technoweenie/attachment_fu/backends/db_file_backend.rb +40 -0
- data/lib/technoweenie/attachment_fu/backends/file_system_backend.rb +157 -0
- data/lib/technoweenie/attachment_fu/backends/s3_backend.rb +413 -0
- data/lib/technoweenie/attachment_fu/processors/core_image_processor.rb +66 -0
- data/lib/technoweenie/attachment_fu/processors/gd2_processor.rb +59 -0
- data/lib/technoweenie/attachment_fu/processors/image_science_processor.rb +80 -0
- data/lib/technoweenie/attachment_fu/processors/mini_magick_processor.rb +142 -0
- data/lib/technoweenie/attachment_fu/processors/rmagick_processor.rb +66 -0
- data/lib/technoweenie/attachment_fu.rb +625 -0
- data/rackspace_cloudfiles.yml.tpl +14 -0
- data/vendor/red_artisan/core_image/filters/color.rb +27 -0
- data/vendor/red_artisan/core_image/filters/effects.rb +31 -0
- data/vendor/red_artisan/core_image/filters/perspective.rb +25 -0
- data/vendor/red_artisan/core_image/filters/quality.rb +25 -0
- data/vendor/red_artisan/core_image/filters/scale.rb +47 -0
- data/vendor/red_artisan/core_image/filters/watermark.rb +32 -0
- data/vendor/red_artisan/core_image/processor.rb +123 -0
- metadata +73 -0
@@ -0,0 +1,212 @@
|
|
1
|
+
module Technoweenie # :nodoc:
|
2
|
+
module AttachmentFu # :nodoc:
|
3
|
+
module Backends
|
4
|
+
# = CloudFiles Storage Backend
|
5
|
+
#
|
6
|
+
# Enables use of {Rackspace Cloud Files}[http://www.mosso.com/cloudfiles.jsp] as a storage mechanism
|
7
|
+
#
|
8
|
+
# Based heavily on the Amazon S3 backend.
|
9
|
+
#
|
10
|
+
# == Requirements
|
11
|
+
#
|
12
|
+
# Requires the {Cloud Files Gem}[http://www.mosso.com/cloudfiles.jsp] by Rackspace
|
13
|
+
#
|
14
|
+
# == Configuration
|
15
|
+
#
|
16
|
+
# Configuration is done via <tt>Rails.root.to_s/config/rackspace_cloudfiles.yml</tt> and is loaded according to the <tt>#{Rails.env}</tt>.
|
17
|
+
# The minimum connection options that you must specify are a container name, your Mosso login name and your Mosso API key.
|
18
|
+
# You can sign up for Cloud Files and get access keys by visiting https://www.mosso.com/buy.htm
|
19
|
+
#
|
20
|
+
# Example configuration (Rails.root.to_s/config/rackspace_cloudfiles.yml)
|
21
|
+
#
|
22
|
+
# development:
|
23
|
+
# container_name: appname_development
|
24
|
+
# username: <your key>
|
25
|
+
# api_key: <your key>
|
26
|
+
#
|
27
|
+
# test:
|
28
|
+
# container_name: appname_test
|
29
|
+
# username: <your key>
|
30
|
+
# api_key: <your key>
|
31
|
+
#
|
32
|
+
# production:
|
33
|
+
# container_name: appname
|
34
|
+
# username: <your key>
|
35
|
+
# apik_key: <your key>
|
36
|
+
#
|
37
|
+
# You can change the location of the config path by passing a full path to the :cloudfiles_config_path option.
|
38
|
+
#
|
39
|
+
# has_attachment :storage => :cloud_files, :cloudfiles_config_path => (Rails.root.to_s + '/config/mosso.yml')
|
40
|
+
#
|
41
|
+
# === Required configuration parameters
|
42
|
+
#
|
43
|
+
# * <tt>:username</tt> - The username for your Rackspace Cloud (Mosso) account. Provided by Rackspace.
|
44
|
+
# * <tt>:secret_access_key</tt> - The api key for your Rackspace Cloud account. Provided by Rackspace.
|
45
|
+
# * <tt>:container_name</tt> - The name of a container in your Cloud Files account.
|
46
|
+
#
|
47
|
+
# If any of these required arguments is missing, a AuthenticationException will be raised from CloudFiles::Connection.
|
48
|
+
#
|
49
|
+
# == Usage
|
50
|
+
#
|
51
|
+
# To specify Cloud Files as the storage mechanism for a model, set the acts_as_attachment <tt>:storage</tt> option to <tt>:cloud_files/tt>.
|
52
|
+
#
|
53
|
+
# class Photo < ActiveRecord::Base
|
54
|
+
# has_attachment :storage => :cloud_files
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# === Customizing the path
|
58
|
+
#
|
59
|
+
# By default, files are prefixed using a pseudo hierarchy in the form of <tt>:table_name/:id</tt>, which results
|
60
|
+
# in Cloud Files object names (and urls) that look like: http://:server/:container_name/:table_name/:id/:filename with :table_name
|
61
|
+
# representing the customizable portion of the path. You can customize this prefix using the <tt>:path_prefix</tt>
|
62
|
+
# option:
|
63
|
+
#
|
64
|
+
# class Photo < ActiveRecord::Base
|
65
|
+
# has_attachment :storage => :cloud_files, :path_prefix => 'my/custom/path'
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# Which would result in public URLs like <tt>http(s)://:server/:container_name/my/custom/path/:id/:filename.</tt>
|
69
|
+
#
|
70
|
+
# === Permissions
|
71
|
+
#
|
72
|
+
# File permisisons are determined by the permissions of the container. At present, the options are public (and distributed
|
73
|
+
# by the Limelight CDN), and private (only available to your login)
|
74
|
+
#
|
75
|
+
# === Other options
|
76
|
+
#
|
77
|
+
# Of course, all the usual configuration options apply, such as content_type and thumbnails:
|
78
|
+
#
|
79
|
+
# class Photo < ActiveRecord::Base
|
80
|
+
# has_attachment :storage => :cloud_files, :content_type => ['application/pdf', :image], :resize_to => 'x50'
|
81
|
+
# has_attachment :storage => :cloud_files, :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
# === Accessing Cloud Files URLs
|
85
|
+
#
|
86
|
+
# You can get an object's public URL using the cloudfiles_url accessor. For example, assuming that for your postcard app
|
87
|
+
# you had a container name like 'postcard_world_development', and an attachment model called Photo:
|
88
|
+
#
|
89
|
+
# @postcard.cloudfiles_url # => http://cdn.cloudfiles.mosso.com/c45182/uploaded_files/20/london.jpg
|
90
|
+
#
|
91
|
+
# The resulting url is in the form: http://:server/:container_name/:table_name/:id/:file.
|
92
|
+
# The optional thumbnail argument will output the thumbnail's filename (if any).
|
93
|
+
#
|
94
|
+
# Additionally, you can get an object's base path relative to the container root using
|
95
|
+
# <tt>base_path</tt>:
|
96
|
+
#
|
97
|
+
# @photo.file_base_path # => uploaded_files/20
|
98
|
+
#
|
99
|
+
# And the full path (including the filename) using <tt>full_filename</tt>:
|
100
|
+
#
|
101
|
+
# @photo.full_filename # => uploaded_files/20/london.jpg
|
102
|
+
#
|
103
|
+
# Niether <tt>base_path</tt> or <tt>full_filename</tt> include the container name as part of the path.
|
104
|
+
# You can retrieve the container name using the <tt>container_name</tt> method.
|
105
|
+
module CloudFileBackend
|
106
|
+
class RequiredLibraryNotFoundError < StandardError; end
|
107
|
+
class ConfigFileNotFoundError < StandardError; end
|
108
|
+
|
109
|
+
def self.included(base) #:nodoc:
|
110
|
+
mattr_reader :container_name, :cloudfiles_config
|
111
|
+
|
112
|
+
begin
|
113
|
+
require 'cloudfiles'
|
114
|
+
rescue LoadError
|
115
|
+
raise RequiredLibraryNotFoundError.new('CloudFiles could not be loaded')
|
116
|
+
end
|
117
|
+
|
118
|
+
begin
|
119
|
+
@@cloudfiles_config_path = base.attachment_options[:cloudfiles_config_path] || (Rails.root.to_s + '/config/rackspace_cloudfiles.yml')
|
120
|
+
@@cloudfiles_config = @@cloudfiles_config = YAML.load(ERB.new(File.read(@@cloudfiles_config_path)).result)[Rails.env].symbolize_keys
|
121
|
+
rescue
|
122
|
+
#raise ConfigFileNotFoundError.new('File %s not found' % @@cloudfiles_config_path)
|
123
|
+
end
|
124
|
+
|
125
|
+
@@container_name = @@cloudfiles_config[:container_name]
|
126
|
+
@@cf = CloudFiles::Connection.new(@@cloudfiles_config[:username], @@cloudfiles_config[:api_key])
|
127
|
+
@@container = @@cf.container(@@container_name)
|
128
|
+
|
129
|
+
base.before_update :rename_file
|
130
|
+
end
|
131
|
+
|
132
|
+
# Overwrites the base filename writer in order to store the old filename
|
133
|
+
def filename=(value)
|
134
|
+
@old_filename = filename unless filename.nil? || @old_filename
|
135
|
+
write_attribute :filename, sanitize_filename(value)
|
136
|
+
end
|
137
|
+
|
138
|
+
# The attachment ID used in the full path of a file
|
139
|
+
def attachment_path_id
|
140
|
+
((respond_to?(:parent_id) && parent_id) || id).to_s
|
141
|
+
end
|
142
|
+
|
143
|
+
# The pseudo hierarchy containing the file relative to the container name
|
144
|
+
# Example: <tt>:table_name/:id</tt>
|
145
|
+
def base_path(thumbnail = nil)
|
146
|
+
file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix]
|
147
|
+
File.join(file_system_path, attachment_path_id)
|
148
|
+
end
|
149
|
+
|
150
|
+
# The full path to the file relative to the container name
|
151
|
+
# Example: <tt>:table_name/:id/:filename</tt>
|
152
|
+
def full_filename(thumbnail = nil)
|
153
|
+
File.join(base_path(thumbnail), thumbnail_name_for(thumbnail))
|
154
|
+
end
|
155
|
+
|
156
|
+
# All public objects are accessible via a GET request to the Cloud Files servers. You can generate a
|
157
|
+
# url for an object using the cloudfiles_url method.
|
158
|
+
#
|
159
|
+
# @photo.cloudfiles_url
|
160
|
+
#
|
161
|
+
# The resulting url is in the CDN URL for the object
|
162
|
+
#
|
163
|
+
# The optional thumbnail argument will output the thumbnail's filename (if any).
|
164
|
+
#
|
165
|
+
# If you are trying to get the URL for a nonpublic container, nil will be returned.
|
166
|
+
def cloudfiles_url(thumbnail = nil)
|
167
|
+
if @@container.public?
|
168
|
+
File.join(@@container.cdn_url, full_filename(thumbnail))
|
169
|
+
else
|
170
|
+
nil
|
171
|
+
end
|
172
|
+
end
|
173
|
+
alias :public_filename :cloudfiles_url
|
174
|
+
|
175
|
+
def create_temp_file
|
176
|
+
write_to_temp_file current_data
|
177
|
+
end
|
178
|
+
|
179
|
+
def current_data
|
180
|
+
@@container.get_object(full_filename).data
|
181
|
+
end
|
182
|
+
|
183
|
+
protected
|
184
|
+
# Called in the after_destroy callback
|
185
|
+
def destroy_file
|
186
|
+
@@container.delete_object(full_filename)
|
187
|
+
end
|
188
|
+
|
189
|
+
def rename_file
|
190
|
+
# Cloud Files doesn't rename right now, so we'll just nuke.
|
191
|
+
return unless @old_filename && @old_filename != filename
|
192
|
+
|
193
|
+
old_full_filename = File.join(base_path, @old_filename)
|
194
|
+
@@container.delete_object(old_full_filename)
|
195
|
+
|
196
|
+
@old_filename = nil
|
197
|
+
true
|
198
|
+
end
|
199
|
+
|
200
|
+
def save_to_storage
|
201
|
+
if save_attachment?
|
202
|
+
@object = @@container.create_object(full_filename)
|
203
|
+
@object.write((temp_path ? File.open(temp_path) : temp_data))
|
204
|
+
end
|
205
|
+
|
206
|
+
@old_filename = nil
|
207
|
+
true
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Technoweenie # :nodoc:
|
2
|
+
module AttachmentFu # :nodoc:
|
3
|
+
module Backends
|
4
|
+
# Methods for DB backed attachments
|
5
|
+
module DbFileBackend
|
6
|
+
def self.included(base) #:nodoc:
|
7
|
+
Object.const_set(:DbFile, Class.new(ActiveRecord::Base)) unless Object.const_defined?(:DbFile)
|
8
|
+
base.belongs_to :db_file, :class_name => '::DbFile', :foreign_key => 'db_file_id'
|
9
|
+
end
|
10
|
+
|
11
|
+
# Creates a temp file with the current db data.
|
12
|
+
def create_temp_file
|
13
|
+
write_to_temp_file current_data
|
14
|
+
end
|
15
|
+
|
16
|
+
# Gets the current data from the database
|
17
|
+
def current_data
|
18
|
+
db_file.data
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
# Destroys the file. Called in the after_destroy callback
|
23
|
+
def destroy_file
|
24
|
+
db_file.destroy if db_file
|
25
|
+
end
|
26
|
+
|
27
|
+
# Saves the data to the DbFile model
|
28
|
+
def save_to_storage
|
29
|
+
if save_attachment?
|
30
|
+
(db_file || build_db_file).data = temp_data
|
31
|
+
db_file.save!
|
32
|
+
self.db_file_id = db_file.id
|
33
|
+
self.class.where(:id => id).update_all(:db_file_id => db_file.id)
|
34
|
+
end
|
35
|
+
true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'digest/sha2'
|
3
|
+
|
4
|
+
module Technoweenie # :nodoc:
|
5
|
+
module AttachmentFu # :nodoc:
|
6
|
+
module Backends
|
7
|
+
# Methods for file system backed attachments
|
8
|
+
module FileSystemBackend
|
9
|
+
def self.included(base) #:nodoc:
|
10
|
+
base.before_update :rename_file
|
11
|
+
end
|
12
|
+
|
13
|
+
# Gets the full path to the filename in this format:
|
14
|
+
#
|
15
|
+
# # This assumes a model name like MyModel
|
16
|
+
# # public/#{table_name} is the default filesystem path
|
17
|
+
# #{Rails.root}/public/my_models/5/blah.jpg
|
18
|
+
#
|
19
|
+
# Overwrite this method in your model to customize the filename.
|
20
|
+
# The optional thumbnail argument will output the thumbnail's filename.
|
21
|
+
def full_filename(thumbnail = nil)
|
22
|
+
file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix].to_s
|
23
|
+
File.join(Rails.root, file_system_path, *partitioned_path(thumbnail_name_for(thumbnail)))
|
24
|
+
end
|
25
|
+
|
26
|
+
# Used as the base path that #public_filename strips off full_filename to create the public path
|
27
|
+
def base_path
|
28
|
+
@base_path ||= File.join(Rails.root, 'public')
|
29
|
+
end
|
30
|
+
|
31
|
+
# The attachment ID used in the full path of a file
|
32
|
+
def attachment_path_id
|
33
|
+
((respond_to?(:parent_id) && parent_id) || id) || 0
|
34
|
+
end
|
35
|
+
|
36
|
+
# Partitions the given path into an array of path components.
|
37
|
+
#
|
38
|
+
# For example, given an <tt>*args</tt> of ["foo", "bar"], it will return
|
39
|
+
# <tt>["0000", "0001", "foo", "bar"]</tt> (assuming that that id returns 1).
|
40
|
+
#
|
41
|
+
# If the id is not an integer, then path partitioning will be performed by
|
42
|
+
# hashing the string value of the id with SHA-512, and splitting the result
|
43
|
+
# into 4 components. If the id a 128-bit UUID (as set by :uuid_primary_key => true)
|
44
|
+
# then it will be split into 2 components.
|
45
|
+
#
|
46
|
+
# To turn this off entirely, set :partition => false.
|
47
|
+
def partitioned_path(*args)
|
48
|
+
if respond_to?(:attachment_options) && attachment_options[:partition] == false
|
49
|
+
args
|
50
|
+
elsif attachment_options[:uuid_primary_key]
|
51
|
+
# Primary key is a 128-bit UUID in hex format. Split it into 2 components.
|
52
|
+
path_id = attachment_path_id.to_s
|
53
|
+
component1 = path_id[0..15] || "-"
|
54
|
+
component2 = path_id[16..-1] || "-"
|
55
|
+
[component1, component2] + args
|
56
|
+
else
|
57
|
+
path_id = attachment_path_id
|
58
|
+
if path_id.is_a?(Integer)
|
59
|
+
# Primary key is an integer. Split it after padding it with 0.
|
60
|
+
("%08d" % path_id).scan(/..../) + args
|
61
|
+
else
|
62
|
+
# Primary key is a String. Hash it, then split it into 4 components.
|
63
|
+
hash = Digest::SHA512.hexdigest(path_id.to_s)
|
64
|
+
[hash[0..31], hash[32..63], hash[64..95], hash[96..127]] + args
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Gets the public path to the file
|
70
|
+
# The optional thumbnail argument will output the thumbnail's filename.
|
71
|
+
def public_filename(thumbnail = nil)
|
72
|
+
full_filename(thumbnail).gsub %r(^#{Regexp.escape(base_path)}), ''
|
73
|
+
end
|
74
|
+
|
75
|
+
def filename=(value)
|
76
|
+
@old_filename = full_filename unless filename.nil? || @old_filename
|
77
|
+
write_attribute :filename, sanitize_filename(value)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Creates a temp file from the currently saved file.
|
81
|
+
def create_temp_file
|
82
|
+
copy_to_temp_file full_filename
|
83
|
+
end
|
84
|
+
|
85
|
+
protected
|
86
|
+
# Destroys the file. Called in the after_destroy callback
|
87
|
+
def destroy_file
|
88
|
+
FileUtils.rm full_filename
|
89
|
+
# remove directory also if it is now empty
|
90
|
+
Dir.rmdir(File.dirname(full_filename)) if (Dir.entries(File.dirname(full_filename))-['.','..']).empty?
|
91
|
+
rescue
|
92
|
+
logger.info "Exception destroying #{full_filename.inspect}: [#{$!.class.name}] #{$1.to_s}"
|
93
|
+
logger.warn $!.backtrace.collect { |b| " > #{b}" }.join("\n")
|
94
|
+
end
|
95
|
+
|
96
|
+
# Renames the given file before saving
|
97
|
+
def rename_file
|
98
|
+
return unless @old_filename && @old_filename != full_filename
|
99
|
+
if save_attachment? && File.exists?(@old_filename)
|
100
|
+
FileUtils.rm @old_filename
|
101
|
+
elsif File.exists?(@old_filename)
|
102
|
+
FileUtils.mv @old_filename, full_filename
|
103
|
+
end
|
104
|
+
@old_filename = nil
|
105
|
+
true
|
106
|
+
end
|
107
|
+
|
108
|
+
# Zoo Patch : Override saves the file into AWS directly
|
109
|
+
def save_to_storage
|
110
|
+
if save_attachment?
|
111
|
+
# This overwrites the file if it exists, maybe have an allow_overwrite option?
|
112
|
+
uploaded = save_to_s3
|
113
|
+
try = 0
|
114
|
+
while try < 5 && !uploaded
|
115
|
+
try = try + 1
|
116
|
+
uploaded = save_to_s3
|
117
|
+
sleep(5)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
@old_filename = nil
|
121
|
+
true
|
122
|
+
end
|
123
|
+
|
124
|
+
def save_to_s3
|
125
|
+
s3_filename = Digest::SHA1.hexdigest(full_filename.gsub(%r(^#{Regexp.escape(base_path)}), ''))
|
126
|
+
img = self
|
127
|
+
if !img.blank?
|
128
|
+
if !img.moved_to_s3
|
129
|
+
begin
|
130
|
+
AWS::S3::DEFAULT_HOST.replace AWS_BUCKET_DEFAULT_HOST
|
131
|
+
AWS::S3::Base.establish_connection!(
|
132
|
+
:access_key_id => AWS_BUCKET_ACCESS_ID_KEY,
|
133
|
+
:secret_access_key => AWS_BUCKET_SECRET_ACCESS_KEY
|
134
|
+
)
|
135
|
+
AWS::S3::S3Object.store(
|
136
|
+
s3_filename,
|
137
|
+
open(temp_path),
|
138
|
+
AWS_BUCKET_NAME,
|
139
|
+
:access => :public_read,
|
140
|
+
:content_type => img.content_type
|
141
|
+
)
|
142
|
+
img.update_attribute(:moved_to_s3,1)
|
143
|
+
return true
|
144
|
+
rescue Exception => ex #Errno::ENOENT
|
145
|
+
return false
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def current_data
|
152
|
+
File.file?(full_filename) ? File.read(full_filename) : nil
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|