-
-
Save ahoward/908899b484b7192e3022b2a083ad260c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# you should rely ONLY in the class level interface in your views, aka | |
# | |
# Asset.thumbnail(path, :dimensions => '42x42').url | |
# | |
# Asset.bw(path).url | |
# | |
# and no lib internals for now - this is under development | |
# | |
# this lib handles a few things in a simple interface. highlights: | |
# | |
# - file ids are md5s of the file contents | |
# | |
# - no file is ever uploaded more than once to cloudinary from anywhere | |
# | |
# - api responses are cached and committed back to git, these are used in all | |
# future builds/lib calls. this avoids even needing to call 'exists?' during | |
# builds if an upload has ever been pushed to cloudinary. | |
# | |
# | |
# | |
# | |
require_relative '../config/boot.rb' | |
require 'fattr' | |
require 'coerce' | |
require 'map' | |
require 'systemu' | |
require 'uuid' | |
require 'util' | |
require 'say' | |
class Asset < ::String | |
# | |
fattr(:processing){ Map.new } | |
fattr(:path) | |
fattr(:identifier) | |
fattr(:response) | |
Fattr(:noop){ ENV['ASSET_NOOP'] } | |
DEFAULT_PROCESSING = Map.new({ | |
:quality => 75, | |
:format => :jpg | |
}) | |
def initialize(path) | |
@path = Asset.find(path) | |
@identifier = nil | |
@response = nil | |
processing.update(DEFAULT_PROCESSING) | |
if @path =~ /\.gif$/ | |
processing[:format] = :gif | |
end | |
super(@path) | |
end | |
def path | |
@path | |
end | |
def Asset.upload(asset) | |
asset.upload | |
end | |
def upload | |
# | |
if((response = metadata)) # NOT A TYPO ;-) | |
@identifier = response['public_id'] or abort("no public_id! in #{ metadata_path }") | |
@response = response | |
return | |
end | |
# | |
return if @response # ALSO NOT A TYPO ;-) | |
# | |
@identifier = Asset.identifier_for(@path) | |
error = nil | |
# | |
3.times do | |
error = nil | |
begin | |
unless ::Cloudinary::Uploader.exists?(identifier) | |
@response = ::Cloudinary::Uploader.upload(path, 'public_id' => identifier) | |
cache_metadata(@response) | |
Say.say("#{ Util.relative_path(path, :from => Asset.root) } #=> #{ @response['url'] }", :color => :red) | |
else | |
unless metadata? | |
@response = ::Cloudinary::Api.resource(identifier) | |
cache_metadata(@response) | |
end | |
end | |
break(@response) | |
rescue => e | |
Say.say("#{ Util.relative_path(path, :from => Asset.root) } #=> #{ e.message } (#{ e.class })", :color => :yellow) | |
error = e | |
sleep(rand * 5) | |
end | |
end | |
# | |
error ? raise(error) : @response | |
end | |
def metadata? | |
if test(?>, path, metadata_path) | |
FileUtils.rm_f(metadata_path) | |
end | |
test(?f, metadata_path) | |
end | |
def metadata | |
YAML.load(IO.binread(metadata_path)) if metadata? | |
end | |
def cache_metadata(metadata) | |
FileUtils.mkdir_p(File.dirname(metadata_path)) | |
IO.binwrite(metadata_path, {}.update(metadata).to_yaml) | |
Say.say("cached metadata at #{ Util.relative_path(metadata_path, :from => Asset.root) } ...", :color => :yellow) | |
end | |
def metadata_path | |
@metadata_path ||= ( | |
relative_path = Util.relative_path(@path, :from => Asset.root).to_s | |
File.join(Asset.metadata_root.to_s, relative_path) + '.cloudinary.yml' | |
) | |
end | |
def upload! | |
@response = nil | |
end | |
# | |
def relative_url | |
relative_url = '/' + Util.relative_path(@path, :from => Asset.root) | |
if relative_url =~ %r{/public/} | |
relative_url.gsub!(%r{/public/}, '/') # FIXME | |
end | |
relative_url | |
end | |
# | |
def url_for(*args) | |
options = Map.options_for!(args) | |
url = relative_url | |
processing = self.processing.merge(options[:processing] || options) | |
begin | |
case | |
when Asset.noop? | |
Say.say("ASSET_NOOP: #{ path } -> #{ url }", :color => :yellow) | |
when image? | |
upload | |
url = ::Cloudinary::Utils.cloudinary_url(identifier, processing) | |
url.sub!(%r{^https?:}, '') # proto-relative... | |
else | |
:do_nothing | |
end | |
url | |
rescue => e | |
Say.say("ASSET_ERROR: #{ path } -> #{ url }\n\n#{ Util.emsg(e) }", :color => :red) | |
relative_url | |
end | |
end | |
def Asset.url(*args, &block) | |
Asset.url_for(*args, &block) | |
end | |
def url(*args, &block) | |
url_for(*args, &block) | |
end | |
# | |
Fattr(:image_re){ %r/\.(png|jpg|jpeg|gif|tif|tiff)$/ } | |
def Asset.image?(path) | |
path.to_s =~ Asset.image_re | |
end | |
def image? | |
Asset.image?(@path) | |
end | |
def black_and_white | |
processing[:effect] ||= {} | |
processing[:effect][:saturation] = -100 | |
self | |
end | |
def bw | |
black_and_white | |
end | |
def Asset.for(*args, &block) | |
new(*args, &block) | |
end | |
def Asset.black_and_white(*args, &block) | |
new(*args, &block).black_and_white | |
end | |
def Asset.bw(*args, &block) | |
black_and_white(*args, &block) | |
end | |
def thumbnail(options = {}) | |
options = Map.for(options) | |
width = options[:width] | |
height = options[:height] | |
dimensions = options[:dimensions] | |
width = width.to_s.scan(/\d+/).first | |
height = height.to_s.scan(/\d+/).first | |
dimensions ||= "#{ width }x#{ height }" | |
width ||= (dimensions.split('x')[0] || 150) | |
height ||= dimensions.split('x')[1] | |
if width | |
processing[:width] = width | |
processing[:crop] = :thumb | |
end | |
if height | |
processing[:height] = height | |
processing[:crop] = :thumb | |
end | |
self | |
end | |
def Asset.thumbnail(*args, &block) | |
new(*args, &block).thumbnail | |
end | |
# | |
Fattr(:lib){ File.expand_path(__FILE__) } | |
Fattr(:libdir){ File.dirname(lib) } | |
Fattr(:root){ File.dirname(libdir) } | |
Fattr(:metadata_root){ File.join(Asset.root, 'tmp/cache/cloudinary') } | |
Fattr(:source_dir){ File.join(root, 'source') } | |
Fattr(:build_dir){ File.join(root, 'build') } | |
Fattr(:public_dir){ File.join(root, 'public') } | |
Fattr(:images_dir){ File.join(source_dir, 'images') } | |
Fattr(:assets_dir){ File.join(build_dir, 'assets') } | |
Fattr(:search_path){ | |
[ | |
source_dir, | |
images_dir, | |
public_dir, | |
build_dir | |
] | |
} | |
class Error < ::StandardError | |
class NotFound < Error | |
end | |
class Ambiguous < Error | |
end | |
end | |
def Asset.find(relative_path) | |
relative_path = relative_path.to_s | |
return relative_path if test(?f, relative_path) | |
prefix = relative_path.split('.', 2).first | |
relative_globs = [relative_path, "#{ prefix }.*"] | |
Asset.search_path.each do |dirname| | |
relative_globs.each do |relative_glob| | |
glob = File.join(dirname, relative_glob) | |
candidates = Dir.glob(glob).select{|entry| entry !~ /\.cloudinary\.yml$/} | |
case candidates.size | |
when 0 | |
next | |
when 1 | |
return(candidates.first) | |
else | |
raise Error::Ambiguous.new("#{ relative_path } (#{ candidates.join(' | ') })") | |
end | |
end | |
end | |
raise Error::NotFound.new(relative_path) | |
end | |
def Asset.search(*relative_paths) | |
relative_paths = Coerce.list_of_strings(relative_paths) | |
relative_paths.each do |relative_path| | |
begin | |
asset = Asset.find(relative_path) | |
return asset | |
rescue Error::NotFound | |
next | |
end | |
end | |
raise Error::NotFound.new(relative_paths.join(', ')) | |
end | |
def Asset.identifier_for(path) | |
@identifiers ||= {} | |
stat = File.stat(path) | |
key = [path, stat.mtime] | |
@identifiers[key] ||= ( | |
Util.md5(IO.binread(path)) | |
) | |
end | |
end | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment