public
Description: A lightweight and opinionated but hackable library for attaching images to ActiveRecord models.
Homepage:
Clone URL: git://github.com/norman/has_image.git
has_image / lib / has_image / storage.rb
100644 178 lines (150 sloc) 5.913 kb
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
require 'active_support'
require 'stringio'
require 'fileutils'
require 'zlib'
 
module HasImage
  
  # Filesystem storage for the HasImage gem. The methods that HasImage inserts
  # into ActiveRecord models only depend on the public methods in this class,
  # so it should be reasonably straightforward to implement a different
  # storage mechanism for Amazon AWS, Photobucket, DBFile, SFTP, or whatever
  # you want.
  class Storage
    
    attr_accessor :image_data, :options, :temp_file
 
    class << self
      
      # {Jamis Buck's well known
      # solution}[http://www.37signals.com/svn/archives2/id_partitioning.php]
      # to this problem fails with high ids, such as those created by
      # db:fixture:load. This version scales to large ids more gracefully.
      # Thanks to Adrian Mugnolo for the fix.
      def partitioned_path(id, *args)
        ["%04d" % ((id.to_i / 1e4) % 1e4), "%04d" % (id.to_i % 1e4)].concat(args)
      end
 
      # Generates a 4-6 character random file name to use for the image and
      # its thumbnails. This is done to avoid having files with unfortunate
      # names. On one of my sites users frequently upload images with Arabic
      # names, and they end up being hard to manipulate on the command line.
      # This also helps prevent a possibly undesirable sitation where the
      # uploaded images have offensive names.
      def generated_file_name
        Zlib.crc32(Time.now.to_s + rand(10e10).to_s).to_s(36).downcase
      end
          
    end
 
    # The constuctor should be invoked with the options set by has_image.
    def initialize(options) # :nodoc:
      @options = options
    end
 
    # The image data can be anything that inherits from IO. If you pass in an
    # instance of Tempfile, it will be used directly without being copied to
    # a new temp file.
    def image_data=(image_data)
      raise StorageError.new if image_data.blank?
      if image_data.is_a?(Tempfile)
        @temp_file = image_data
      else
        image_data.rewind
        @temp_file = Tempfile.new 'has_image_data_%s' % Storage.generated_file_name
        @temp_file.write(image_data.read)
      end
    end
 
    # Is uploaded file smaller than the allowed minimum?
    def image_too_small?
      @temp_file.open if @temp_file.closed?
      @temp_file.size < options[:min_size]
    end
    
    # Is uploaded file larger than the allowed maximum?
    def image_too_big?
      @temp_file.open if @temp_file.closed?
      @temp_file.size > options[:max_size]
    end
    
    # A tip of the hat to attachment_fu.
    alias uploaded_data= image_data=
    
    # A tip of the hat to attachment_fu.
    alias uploaded_data image_data
    
    # Invokes the processor to resize the image(s) and the installs them to
    # the appropriate directory.
    def install_images(id)
      generated_name = Storage.generated_file_name
      install_main_image(id, generated_name)
      install_thumbnails(id, generated_name) if !options[:thumbnails].empty?
      return generated_name
    ensure
      @temp_file.close! if !@temp_file.closed?
      @temp_file = nil
    end
    
    # Gets the "web" path for an image. For example:
    #
    # /photos/0000/0001/3er0zs.jpg
    def public_path_for(object, thumbnail = nil)
      filesystem_path_for(object, thumbnail).gsub(/\A.*public/, '')
    end
    
    # Deletes the images and directory that contains them.
    def remove_images(id)
      FileUtils.rm_r path_for(id)
    end
 
    # Is the uploaded file within the min and max allowed sizes?
    def valid?
      !(image_too_small? || image_too_big?)
    end
 
    protected
 
    # Gets the extension to append to the image. Transforms "jpeg" to "jpg."
    def extension
      options[:convert_to].to_s.downcase.gsub("jpeg", "jpg")
    end
    
    private
    
    # File name, plus thumbnail suffix, plus extension. For example:
    #
    # file_name_for("abc123", :thumb)
    #
    # gives you:
    #
    # "abc123_thumb.jpg"
    #
    #
    def file_name_for(*args)
      "%s.%s" % [args.compact.join("_"), extension]
    end
 
    # Gets the full local filesystem path for an image. For example:
    #
    # /var/sites/example.com/production/public/photos/0000/0001/3er0zs.jpg
    def filesystem_path_for(object, thumbnail = nil)
      File.join(path_for(object.id), file_name_for(object.has_image_file, thumbnail))
    end
    
    # Write the main image to the install directory - probably somewhere under
    # RAILS_ROOT/public.
    def install_main_image(id, name)
      FileUtils.mkdir_p path_for(id)
      main = processor.resize(@temp_file, @options[:resize_to])
      main.tempfile.close
      file = File.open(File.join(path_for(id), file_name_for(name)), "w")
      file.write(IO.read(main.tempfile.path))
      file.close
      main.tempfile.close!
    end
    
    # Write the thumbnails to the install directory - probably somewhere under
    # RAILS_ROOT/public.
    def install_thumbnails(id, name)
      FileUtils.mkdir_p path_for(id)
      path = File.join(path_for(id), file_name_for(name))
      options[:thumbnails].each do |thumb_name, size|
        thumb = processor.resize(path, size)
        thumb.tempfile.close
        file = File.open(File.join(path_for(id), file_name_for(name, thumb_name)), "w")
        file.write(IO.read(thumb.tempfile.path))
        file.close
        thumb.tempfile.close!
      end
    end
 
    # Get the full path for the id. For example:
    #
    # /var/sites/example.org/production/public/photos/0000/0001
    def path_for(id)
      File.join(options[:base_path], options[:path_prefix], Storage.partitioned_path(id))
    end
    
    # Instantiates the processor using the options set in my contructor (if
    # not already instantiated), stores it in an instance variable, and
    # returns it.
    def processor
      @processor ||= Processor.new(options)
    end
    
  end
  
end