updated top the the new version of attachment_fu plugin to work out some
[selectricity-live] / vendor / plugins / attachment_fu / lib / technoweenie / attachment_fu / backends / file_system_backend.rb
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           # Saves the file to the file system
109           def save_to_storage
110             if save_attachment?
111               # TODO: This overwrites the file if it exists, maybe have an allow_overwrite option?
112               FileUtils.mkdir_p(File.dirname(full_filename))
113               FileUtils.cp(temp_path, full_filename)
114               FileUtils.chmod(attachment_options[:chmod] || 0644, full_filename)
115             end
116             @old_filename = nil
117             true
118           end
119           
120           def current_data
121             File.file?(full_filename) ? File.read(full_filename) : nil
122           end
123       end
124     end
125   end
126 end

Benjamin Mako Hill || Want to submit a patch?