/
database.rb
216 lines (200 loc) · 8.37 KB
/
database.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
module Paperclip
module Storage
# Store files in a database.
#
# Usage is identical to the file system storage version, except:
#
# 1. In your model specify the "database" storage option; for example:
# has_attached_file :avatar, :storage => :database
#
# The files will be stored in a new database table named with the plural attachment name
# by default, "avatars" in this example.
#
# 2. You need to create this new storage table with at least these columns:
# - file_contents
# - style
# - the primary key for the parent model (e.g. user_id)
#
# Note the "binary" migration will not work for the LONGBLOG type in MySQL for the
# file_cotents column. You may need to craft a SQL statement for your migration,
# depending on which database server you are using. Here's an example migration for MySQL:
#
# create_table :avatars do |t|
# t.string :style
# t.integer :user_id
# t.timestamps
# end
# execute 'ALTER TABLE avatars ADD COLUMN file_contents LONGBLOB'
#
# You can optionally specify any storage table name you want and whether or not deletion is done by cascading or not as follows:
# has_attached_file :avatar, :storage => :database, :database_table => 'avatar_files', :cascade_deletion => true
#
# 3. By default, URLs will be set to this pattern:
# /:relative_root/:class/:attachment/:id?style=:style
#
# Example:
# /app-root-url/users/avatars/23?style=original
#
# The idea here is that to retrieve a file from the database storage, you will need some
# controller's code to be executed.
#
# Once you pick a controller to use for downloading, you can add this line
# to generate the download action for the default URL/action (the plural attachment name),
# "avatars" in this example:
# downloads_files_for :user, :avatar
#
# Or you can write a download method manually if there are security, logging or other
# requirements.
#
# If you prefer a different URL for downloading files you can specify that in the model; e.g.:
# has_attached_file :avatar, :storage => :database, :url => '/users/show_avatar/:id/:style'
#
# 4. Add a route for the download to the controller which will handle downloads, if necessary.
#
# The default URL, /:relative_root/:class/:attachment/:id?style=:style, will be matched by
# the default route: :controller/:action/:id
#
module Database
def self.extended(base)
base.instance_eval do
setup_paperclip_files_model
override_default_options base
end
Paperclip.interpolates(:database_path) do |attachment, style|
attachment.database_path(style)
end
Paperclip.interpolates(:relative_root) do |attachment, style|
begin
if ActionController::AbstractRequest.respond_to?(:relative_url_root)
relative_url_root = ActionController::AbstractRequest.relative_url_root
end
rescue NameError
end
if !relative_url_root && ActionController::Base.respond_to?(:relative_url_root)
ActionController::Base.relative_url_root
end
end
ActiveRecord::Base.logger.info("[paperclip] Database Storage Initalized.")
end
def setup_paperclip_files_model
#TODO: This fails when your model is in a namespace.
@paperclip_files = "#{instance.class.name.underscore}_#{name.to_s}_paperclip_files"
if !Object.const_defined?(@paperclip_files.classify)
@paperclip_file = Object.const_set(@paperclip_files.classify, Class.new(::ActiveRecord::Base))
@paperclip_file.table_name = @options[:database_table] || name.to_s.pluralize
@paperclip_file.validates_uniqueness_of :style, :scope => instance.class.table_name.classify.underscore + '_id'
case Rails::VERSION::STRING
when /^2/
@paperclip_file.named_scope :file_for, lambda {|style| { :conditions => ['style = ?', style] }}
else # 3.x
@paperclip_file.scope :file_for, lambda {|style| @paperclip_file.where('style = ?', style) }
end
else
@paperclip_file = Object.const_get(@paperclip_files.classify)
end
@database_table = @paperclip_file.table_name
#FIXME: This fails when using set_table_name "<myname>" in your model
#FIXME: This should be fixed in ActiveRecord...
instance.class.has_many @paperclip_files.to_sym, :foreign_key => instance.class.table_name.classify.underscore + '_id'
end
private :setup_paperclip_files_model
def copy_to_local_file(style, dest_path)
File.open(dest_path, 'wb+'){|df| to_file(style).tap{|sf| File.copy_stream(sf, df); sf.close;sf.unlink} }
end
def override_default_options(base)
if @options[:url] == base.class.default_options[:url]
@options[:url] = ":relative_root/:class/:attachment/:id?style=:style"
end
@options[:path] = ":database_path"
end
private :override_default_options
def database_table
@database_table
end
def database_path(style)
paperclip_file = file_for(style)
if paperclip_file
"#{database_table}(id=#{paperclip_file.id},style=#{style.to_s})"
else
"#{database_table}(id=new,style=#{style.to_s})"
end
end
def exists?(style = default_style)
if original_filename
!file_for(style).nil?
else
false
end
end
# Returns representation of the data of the file assigned to the given
# style, in the format most representative of the current storage.
def to_file style = default_style
if @queued_for_write[style]
@queued_for_write[style]
elsif exists?(style)
tempfile = Tempfile.new instance_read(:file_name)
tempfile.binmode
tempfile.write file_contents(style)
tempfile.flush
tempfile.rewind
tempfile
else
nil
end
end
alias_method :to_io, :to_file
def file_for(style)
db_result = instance.send("#{@paperclip_files}").send(:file_for, style.to_s)
raise RuntimeError, "More than one result for #{style}" if db_result.size > 1
db_result.first
end
def file_contents(style)
file_for(style).file_contents
end
def flush_writes
ActiveRecord::Base.logger.info("[paperclip] Writing files for #{name}")
@queued_for_write.each do |style, file|
case Rails::VERSION::STRING
when /^2/, /^3/
paperclip_file = instance.send(@paperclip_files).send(:find_or_create_by_style, style.to_s)
when /^4/
paperclip_file = instance.send(@paperclip_files).send(:find_or_create_by, style: style.to_s)
else # 4.x
raise "Rails version #{Rails::VERSION::STRING} is not supported (yet)"
end
paperclip_file.file_contents = file.read
paperclip_file.save!
instance.reload
end
@queued_for_write = {}
end
def flush_deletes #:nodoc:
ActiveRecord::Base.logger.info("[paperclip] Deleting files for #{name}")
@queued_for_delete.uniq! ##This is apparently necessary for paperclip v 3.x
@queued_for_delete.each do |path|
/id=([0-9]+)/.match(path)
if @options[:cascade_deletion] && !instance.class.exists?(instance.id)
raise RuntimeError, "Deletion has not been done by through cascading." if @paperclip_file.exists?($1)
else
@paperclip_file.destroy $1
end
end
@queued_for_delete = []
end
module ControllerClassMethods
def self.included(base)
base.extend(self)
end
def downloads_files_for(model, attachment, options = {})
define_method("#{attachment.to_s.pluralize}") do
model_record = Object.const_get(model.to_s.camelize.to_sym).find(params[:id])
style = params[:style] ? params[:style] : 'original'
send_data model_record.send(attachment).file_contents(style),
:filename => model_record.send("#{attachment}_file_name".to_sym),
:type => model_record.send("#{attachment}_content_type".to_sym)
end
end
end
end
end
end