Created
April 26, 2015 18:38
-
-
Save ahoward/22b8cad843257da7f177 to your computer and use it in GitHub Desktop.
lib/asset.rb - minimal way of interfacing with a cdn that can store files and process them on the fly - in this case cloudinary, but applicable to others
This file contains hidden or 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 leve 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 have 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){ Hash.new } | |
| fattr(:identifier) | |
| fattr(:response) | |
| DEFAULT_PROCESSING = { | |
| :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 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 url_for(processing = {}) | |
| options = self.processing.merge(processing) | |
| upload | |
| url = ::Cloudinary::Utils.cloudinary_url(identifier, options) | |
| url.sub(%r{^https?:}, '') # proto-relative... | |
| end | |
| def url(*args, &block) | |
| url_for(*args, &block) | |
| 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