github
Advanced Search
  • Home
  • Pricing and Signup
  • Explore GitHub
  • Blog
  • Login

patshaughnessy / paperclip forked from thoughtbot/paperclip

  • Admin
  • Watch Unwatch
  • Fork
  • Your Fork
  • Pull Request
  • Download Source
    • 17
    • 294
  • Source
  • Commits
  • Network (294)
  • Issues (0)
  • Downloads (16)
  • Wiki (1)
  • Graphs
  • Tree: 9556fab

click here to add a description

click here to add a homepage

  • Branches (1)
    • master
  • Tags (16)
    • v2.2.6
    • v2.2.5
    • v2.2.4
    • v2.2.3
    • v2.2.2
    • v2.2.1
    • v2.2.0
    • v2.1.5
    • v2.1.4
    • v2.1.3
    • v2.1.2
    • v2.1.1
    • v2.1.0
    • v2.0.2
    • v2.0.1
    • v2.0.0
Sending Request…
Enable Donations

Pledgie Donations

Once activated, we'll place the following badge in your repository's detail box:
Pledgie_example
This service is courtesy of Pledgie.

Forked to store files in a database table — Read more

  cancel

http://patshaughnessy.net/paperclip-database-storage

  cancel
  • Private
  • Read-Only
  • HTTP Read-Only

This URL has Read+Write access

Updated comment/doc for database storage 
patshaughnessy (author)
Tue Apr 14 11:09:47 -0700 2009
commit  9556fab3e1659a5c8690db82394cea55714b5da4
tree    e8fe4e46951c8ae301d9608c5eea8678310110b1
parent  d96f1e48b859a566c7e51140677b8179be55d0ae
paperclip / lib / paperclip / storage.rb lib/paperclip/storage.rb
100644 433 lines (397 sloc) 18.882 kb
edit raw blame history
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
module Paperclip
  module Storage
 
    # The default place to store attachments is in the filesystem. Files on the local
    # filesystem can be very easily served by Apache without requiring a hit to your app.
    # They also can be processed more easily after they've been saved, as they're just
    # normal files. There is one Filesystem-specific option for has_attached_file.
    # * +path+: The location of the repository of attachments on disk. This can (and, in
    # almost all cases, should) be coordinated with the value of the +url+ option to
    # allow files to be saved into a place where Apache can serve them without
    # hitting your app. Defaults to
    # ":rails_root/public/:attachment/:id/:style/:basename.:extension"
    # By default this places the files in the app's public directory which can be served
    # directly. If you are using capistrano for deployment, a good idea would be to
    # make a symlink to the capistrano-created system directory from inside your app's
    # public directory.
    # See Paperclip::Attachment#interpolate for more information on variable interpolaton.
    # :path => "/var/app/attachments/:class/:id/:style/:basename.:extension"
    module Filesystem
      def self.extended base
      end
      
      def exists?(style = default_style)
        if original_filename
          File.exist?(path(style))
        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
        @queued_for_write[style] || (File.new(path(style), 'rb') if exists?(style))
      end
      alias_method :to_io, :to_file
 
      def flush_writes #:nodoc:
        @queued_for_write.each do |style, file|
          file.close
          FileUtils.mkdir_p(File.dirname(path(style)))
          logger.info("[paperclip] saving #{path(style)}")
          FileUtils.mv(file.path, path(style))
          FileUtils.chmod(0644, path(style))
        end
        @queued_for_write = {}
      end
 
      def flush_deletes #:nodoc:
        @queued_for_delete.each do |path|
          begin
            logger.info("[paperclip] deleting #{path}")
            FileUtils.rm(path) if File.exist?(path)
          rescue Errno::ENOENT => e
            # ignore file-not-found, let everything else pass
          end
          begin
            while(true)
              path = File.dirname(path)
              FileUtils.rmdir(path)
            end
          rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR
            # Stop trying to remove parent directories
          rescue SystemCallError => e
            logger.info("[paperclip] There was an unexpected error while deleting directories: #{e.class}")
            # Ignore it
          end
        end
        @queued_for_delete = []
      end
    end
 
    # Amazon's S3 file hosting service is a scalable, easy place to store files for
    # distribution. You can find out more about it at http://aws.amazon.com/s3
    # There are a few S3-specific options for has_attached_file:
    # * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
    # to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
    # gives you. You can 'environment-space' this just like you do to your
    # database.yml file, so different environments can use different accounts:
    # development:
    # access_key_id: 123...
    # secret_access_key: 123...
    # test:
    # access_key_id: abc...
    # secret_access_key: abc...
    # production:
    # access_key_id: 456...
    # secret_access_key: 456...
    # This is not required, however, and the file may simply look like this:
    # access_key_id: 456...
    # secret_access_key: 456...
    # In which case, those access keys will be used in all environments. You can also
    # put your bucket name in this file, instead of adding it to the code directly.
    # This is useful when you want the same account but a different bucket for
    # development versus production.
    # * +s3_permissions+: This is a String that should be one of the "canned" access
    # policies that S3 provides (more information can be found here:
    # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
    # The default for Paperclip is "public-read".
    # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
    # 'http' or 'https'. Defaults to 'http' when your :s3_permissions are 'public-read' (the
    # default), and 'https' when your :s3_permissions are anything else.
    # * +s3_headers+: A hash of headers such as {'Expires' => 1.year.from_now.httpdate}
    # * +bucket+: This is the name of the S3 bucket that will store your files. Remember
    # that the bucket must be unique across all of Amazon S3. If the bucket does not exist
    # Paperclip will attempt to create it. The bucket name will not be interpolated.
    # You can define the bucket as a Proc if you want to determine it's name at runtime.
    # Paperclip will call that Proc with attachment as the only argument.
    # * +s3_host_alias+: The fully-qualified domain name (FQDN) that is the alias to the
    # S3 domain of your bucket. Used with the :s3_alias_url url interpolation. See the
    # link in the +url+ entry for more information about S3 domains and buckets.
    # * +url+: There are three options for the S3 url. You can choose to have the bucket's name
    # placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
    # Lastly, you can specify a CNAME (which requires the CNAME to be specified as
    # :s3_alias_url. You can read more about CNAMEs and S3 at
    # http://docs.amazonwebservices.com/AmazonS3/latest/index.html?VirtualHosting.html
    # Normally, this won't matter in the slightest and you can leave the default (which is
    # path-style, or :s3_path_url). But in some cases paths don't work and you need to use
    # the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
    # NOTE: If you use a CNAME for use with CloudFront, you can NOT specify https as your
    # :s3_protocol; This is *not supported* by S3/CloudFront. Finally, when using the host
    # alias, the :bucket parameter is ignored, as the hostname is used as the bucket name
    # by S3.
    # * +path+: This is the key under the bucket in which the file will be stored. The
    # URL will be constructed from the bucket and the path. This is what you will want
    # to interpolate. Keys should be unique, like filenames, and despite the fact that
    # S3 (strictly speaking) does not support directories, you can still use a / to
    # separate parts of your file name.
    module S3
      def self.extended base
        require 'right_aws'
        base.instance_eval do
          @s3_credentials = parse_credentials(@options[:s3_credentials])
          @bucket = @options[:bucket] || @s3_credentials[:bucket]
          @bucket = @bucket.call(self) if @bucket.is_a?(Proc)
          @s3_options = @options[:s3_options] || {}
          @s3_permissions = @options[:s3_permissions] || 'public-read'
          @s3_protocol = @options[:s3_protocol] || (@s3_permissions == 'public-read' ? 'http' : 'https')
          @s3_headers = @options[:s3_headers] || {}
          @s3_host_alias = @options[:s3_host_alias]
          @url = ":s3_path_url" unless @url.to_s.match(/^:s3.*url$/)
        end
        base.class.interpolations[:s3_alias_url] = lambda do |attachment, style|
          "#{attachment.s3_protocol}://#{attachment.s3_host_alias}/#{attachment.path(style).gsub(%r{^/}, "")}"
        end
        base.class.interpolations[:s3_path_url] = lambda do |attachment, style|
          "#{attachment.s3_protocol}://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
        end
        base.class.interpolations[:s3_domain_url] = lambda do |attachment, style|
          "#{attachment.s3_protocol}://#{attachment.bucket_name}.s3.amazonaws.com/#{attachment.path(style).gsub(%r{^/}, "")}"
        end
      end
 
      def s3
        @s3 ||= RightAws::S3.new(@s3_credentials[:access_key_id],
                                 @s3_credentials[:secret_access_key],
                                 @s3_options)
      end
 
      def s3_bucket
        @s3_bucket ||= s3.bucket(@bucket, true, @s3_permissions)
      end
 
      def bucket_name
        @bucket
      end
 
      def s3_host_alias
        @s3_host_alias
      end
 
      def parse_credentials creds
        creds = find_credentials(creds).stringify_keys
        (creds[ENV['RAILS_ENV']] || creds).symbolize_keys
      end
      
      def exists?(style = default_style)
        s3_bucket.key(path(style)) ? true : false
      end
 
      def s3_protocol
        @s3_protocol
      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
        @queued_for_write[style] || s3_bucket.key(path(style))
      end
      alias_method :to_io, :to_file
 
      def flush_writes #:nodoc:
        @queued_for_write.each do |style, file|
          begin
            logger.info("[paperclip] saving #{path(style)}")
            key = s3_bucket.key(path(style))
            key.data = file
            key.put(nil, @s3_permissions, {'Content-type' => instance_read(:content_type)}.merge(@s3_headers))
          rescue RightAws::AwsError => e
            raise
          end
        end
        @queued_for_write = {}
      end
 
      def flush_deletes #:nodoc:
        @queued_for_delete.each do |path|
          begin
            logger.info("[paperclip] deleting #{path}")
            if file = s3_bucket.key(path)
              file.delete
            end
          rescue RightAws::AwsError
            # Ignore this.
          end
        end
        @queued_for_delete = []
      end
      
      def find_credentials creds
        case creds
        when File
          YAML.load_file(creds.path)
        when String
          YAML.load_file(creds)
        when Hash
          creds
        else
          raise ArgumentError, "Credentials are not a path, file, or hash."
        end
      end
      private :find_credentials
 
    end
 
    # 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
    #
    # 2. The file will be stored in a column called [attachment name]_file (e.g. "avatar_file") by default.
    #
    # To specify a different column name, use :column, like this:
    # has_attached_file :avatar, :storage => :database, :column => 'avatar_data'
    #
    # If you have defined different styles, these files will be stored in additional columns called
    # [attachment name]_[style name]_file (e.g. "avatar_thumb_file") by default.
    #
    # To specify different column names for styles, use :column in the style definition, like this:
    # has_attached_file :avatar,
    # :storage => :database,
    # :styles => {
    # :medium => {:geometry => "300x300>", :column => 'medium_file'},
    # :thumb => {:geometry => "100x100>", :column => 'thumb_file'}
    # }
    #
    # 3. You need to create these new columns in your migrations or you'll get an exception. Example:
    # add_column :users, :avatar_file, :binary
    # add_column :users, :avatar_medium_file, :binary
    # add_column :users, :avatar_thumb_file, :binary
    #
    # Note the "binary" migration will not work for the LONGBLOB type in MySQL for the
    # file_contents 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:
    # execute 'ALTER TABLE users ADD COLUMN avatar_file LONGBLOB'
    # execute 'ALTER TABLE users ADD COLUMN avatar_medium_file LONGBLOB'
    # execute 'ALTER TABLE users ADD COLUMN avatar_thumb_file LONGBLOB'
    #
    # 4. To avoid performance problems loading all of the BLOB columns every time you access
    # your ActiveRecord object, a class method is provided on your model called
    # “select_without_file_columns_for.” This is set to a :select scope hash that will
    # instruct ActiveRecord::Base.find to load all of the columns except the BLOB/file data columns.
    #
    # If you’re using Rails 2.3, you can specify this as a default scope:
    # default_scope select_without_file_columns_for(:avatar)
    #
    # Or if you’re using Rails 2.1 or 2.2 you can use it to create a named scope:
    # named_scope :without_file_data, select_without_file_columns_for(:avatar)
    #
    # 5. 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'
    #
    # 6. 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
          @file_columns = @options[:file_columns]
          if @url == base.class.default_options[:url]
            @url = ":relative_root/:class/:attachment/:id?style=:style"
          end
        end
        base.class.interpolations[:relative_root] = lambda 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)
            relative_url_root = ActionController::Base.relative_url_root
          end
          relative_url_root
        end
        ActiveRecord::Base.logger.info("[paperclip] Database Storage Initalized.")
      end
 
      def column_for_style style
        @file_columns[style.to_sym]
      end
        
      def instance_read_file(style)
        column = column_for_style(style)
        responds = instance.respond_to?(column)
        cached = self.instance_variable_get("@_#{column}")
        return cached if cached
        # The blob attribute will not be present if select_without_file_columns_for was used
        instance.reload :select => column if !instance.attribute_present?(column) && !instance.new_record?
        instance.send(column) if responds
      end
 
      def instance_write_file(style, value)
        setter = :"#{column_for_style(style)}="
        responds = instance.respond_to?(setter)
        self.instance_variable_set("@_#{setter.to_s.chop}", value)
        instance.send(setter, value) if responds
      end
 
      def file_contents(style = default_style)
        instance_read_file(style)
      end
      alias_method :data, :file_contents
 
      def exists?(style = default_style)
        !file_contents(style).nil?
      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.write file_contents(style)
          tempfile
        else
          nil
        end
      end
      alias_method :to_io, :to_file
 
      def path style = default_style
        original_filename.nil? ? nil : column_for_style(style)
      end
 
      def assign uploaded_file
 
        # Assign standard metadata attributes and perform post processing as usual
        super
 
        # Save the file contents for all styles in ActiveRecord immediately (before save)
        @queued_for_write.each do |style, file|
          instance_write_file(style, file.read)
        end
        
        # If we are assigning another Paperclip attachment, then fixup the
        # filename and content type; necessary since Tempfile is used in to_file
        if uploaded_file.is_a?(Paperclip::Attachment)
          instance_write(:file_name, uploaded_file.instance_read(:file_name))
          instance_write(:content_type, uploaded_file.instance_read(:content_type))
        end
      end
 
      def queue_existing_for_delete
        [:original, *@styles.keys].uniq.each do |style|
          instance_write_file(style, nil)
        end
        instance_write(:file_name, nil)
        instance_write(:content_type, nil)
        instance_write(:file_size, nil)
        instance_write(:updated_at, nil)
      end
 
      def flush_writes
        @queued_for_write = {}
      end
 
      def flush_deletes
        @queued_for_delete = []
      end
      
      module ControllerClassMethods
        def self.included(base)
          base.extend(self)
        end
        def downloads_files_for(model, attachment)
          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
 
Blog | Support | Training | Contact | API | Status | Twitter | Help | Security
© 2010 GitHub Inc. All rights reserved. | Terms of Service | Privacy Policy
Powered by the Dedicated Servers and
Cloud Computing of Rackspace Hosting®
Dedicated Server