Skip to content

Instantly share code, notes, and snippets.

@ahoward
Last active March 4, 2018 23:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ahoward/908899b484b7192e3022b2a083ad260c to your computer and use it in GitHub Desktop.
Save ahoward/908899b484b7192e3022b2a083ad260c to your computer and use it in GitHub Desktop.
# 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