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
|
require 'pathname'
require 'active_support/core_ext/string/multibyte'
begin
# Use mime/types/columnar if available, for reduced memory usage
require 'mime/types/columnar'
rescue LoadError
require 'mime/types'
end
module CarrierWave
##
# SanitizedFile is a base class which provides a common API around all
# the different quirky Ruby File libraries. It has support for Tempfile,
# File, StringIO, Merb-style upload Hashes, as well as paths given as
# Strings and Pathnames.
#
# It's probably needlessly comprehensive and complex. Help is appreciated.
#
class SanitizedFile
attr_reader :file
class << self
attr_writer :sanitize_regexp
def sanitize_regexp
@sanitize_regexp ||= /[^[:word:]\.\-\+]/
end
end
def initialize(file)
self.file = file
@content = nil
end
##
# Returns the filename as is, without sanitizing it.
#
# === Returns
#
# [String] the unsanitized filename
#
def original_filename
return @original_filename if @original_filename
if @file and @file.respond_to?(:original_filename)
@file.original_filename
elsif path
File.basename(path)
end
end
##
# Returns the filename, sanitized to strip out any evil characters.
#
# === Returns
#
# [String] the sanitized filename
#
def filename
sanitize(original_filename) if original_filename
end
alias_method :identifier, :filename
##
# Returns the part of the filename before the extension. So if a file is called 'test.jpeg'
# this would return 'test'
#
# === Returns
#
# [String] the first part of the filename
#
def basename
split_extension(filename)[0] if filename
end
##
# Returns the file extension
#
# === Returns
#
# [String] the extension
#
def extension
split_extension(filename)[1] if filename
end
##
# Returns the file's size.
#
# === Returns
#
# [Integer] the file's size in bytes.
#
def size
if is_path?
exists? ? File.size(path) : 0
elsif @file.respond_to?(:size)
@file.size
elsif path
exists? ? File.size(path) : 0
else
0
end
end
##
# Returns the full path to the file. If the file has no path, it will return nil.
#
# === Returns
#
# [String, nil] the path where the file is located.
#
def path
return if @file.blank?
if is_path?
File.expand_path(@file)
elsif @file.respond_to?(:path) && !@file.path.blank?
File.expand_path(@file.path)
end
end
##
# === Returns
#
# [Boolean] whether the file is supplied as a pathname or string.
#
def is_path?
!!((@file.is_a?(String) || @file.is_a?(Pathname)) && !@file.blank?)
end
##
# === Returns
#
# [Boolean] whether the file is valid and has a non-zero size
#
def empty?
@file.nil? || self.size.nil? || (self.size.zero? && ! self.exists?)
end
##
# === Returns
#
# [Boolean] Whether the file exists
#
def exists?
self.path.present? && File.exist?(self.path)
end
##
# Returns the contents of the file.
#
# === Returns
#
# [String] contents of the file
#
def read
if @content
@content
elsif is_path?
File.open(@file, "rb") {|file| file.read}
else
@file.try(:rewind)
@content = @file.read
@file.try(:close) unless @file.try(:closed?)
@content
end
end
##
# Moves the file to the given path
#
# === Parameters
#
# [new_path (String)] The path where the file should be moved.
# [permissions (Integer)] permissions to set on the file in its new location.
# [directory_permissions (Integer)] permissions to set on created directories.
#
def move_to(new_path, permissions=nil, directory_permissions=nil, keep_filename=false)
return if self.empty?
new_path = File.expand_path(new_path)
mkdir!(new_path, directory_permissions)
move!(new_path)
chmod!(new_path, permissions)
if keep_filename
self.file = {:tempfile => new_path, :filename => original_filename, :content_type => content_type}
else
self.file = {:tempfile => new_path, :content_type => content_type}
end
self
end
##
# Helper to move file to new path.
#
def move!(new_path)
if exists?
FileUtils.mv(path, new_path) unless File.identical?(new_path, path)
else
File.open(new_path, "wb") { |f| f.write(read) }
end
end
##
# Creates a copy of this file and moves it to the given path. Returns the copy.
#
# === Parameters
#
# [new_path (String)] The path where the file should be copied to.
# [permissions (Integer)] permissions to set on the copy
# [directory_permissions (Integer)] permissions to set on created directories.
#
# === Returns
#
# @return [CarrierWave::SanitizedFile] the location where the file will be stored.
#
def copy_to(new_path, permissions=nil, directory_permissions=nil)
return if self.empty?
new_path = File.expand_path(new_path)
mkdir!(new_path, directory_permissions)
copy!(new_path)
chmod!(new_path, permissions)
self.class.new({:tempfile => new_path, :content_type => content_type})
end
##
# Helper to create copy of file in new path.
#
def copy!(new_path)
if exists?
FileUtils.cp(path, new_path) unless new_path == path
else
File.open(new_path, "wb") { |f| f.write(read) }
end
end
##
# Removes the file from the filesystem.
#
def delete
FileUtils.rm(self.path) if exists?
end
##
# Returns a File object, or nil if it does not exist.
#
# === Returns
#
# [File] a File object representing the SanitizedFile
#
def to_file
return @file if @file.is_a?(File)
File.open(path, "rb") if exists?
end
##
# Returns the content type of the file.
#
# === Returns
#
# [String] the content type of the file
#
def content_type
return @content_type if @content_type
if @file.respond_to?(:content_type) and @file.content_type
@content_type = @file.content_type.to_s.chomp
elsif path
@content_type = ::MIME::Types.type_for(path).first.to_s
end
end
##
# Sets the content type of the file.
#
# === Returns
#
# [String] the content type of the file
#
def content_type=(type)
@content_type = type
end
##
# Used to sanitize the file name. Public to allow overriding for non-latin characters.
#
# === Returns
#
# [Regexp] the regexp for sanitizing the file name
#
def sanitize_regexp
CarrierWave::SanitizedFile.sanitize_regexp
end
private
def file=(file)
if file.is_a?(Hash)
@file = file["tempfile"] || file[:tempfile]
@original_filename = file["filename"] || file[:filename]
@content_type = file["content_type"] || file[:content_type] || file["type"] || file[:type]
else
@file = file
@original_filename = nil
@content_type = nil
end
end
# create the directory if it doesn't exist
def mkdir!(path, directory_permissions)
options = {}
options[:mode] = directory_permissions if directory_permissions
FileUtils.mkdir_p(File.dirname(path), **options) unless File.exist?(File.dirname(path))
end
def chmod!(path, permissions)
File.chmod(permissions, path) if permissions
end
# Sanitize the filename, to prevent hacking
def sanitize(name)
name = name.tr("\\", "/") # work-around for IE
name = File.basename(name)
name = name.gsub(sanitize_regexp,"_")
name = "_#{name}" if name =~ /\A\.+\z/
name = "unnamed" if name.size == 0
return name.mb_chars.to_s
end
def split_extension(filename)
# regular expressions to try for identifying extensions
extension_matchers = [
/\A(.+)\.(tar\.([glx]?z|bz2))\z/, # matches "something.tar.gz"
/\A(.+)\.([^\.]+)\z/ # matches "something.jpg"
]
extension_matchers.each do |regexp|
if filename =~ regexp
return $1, $2
end
end
return filename, "" # In case we weren't able to split the extension
end
end # SanitizedFile
end # CarrierWave
|