cloudinary 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +46 -0
- data/Rakefile +3 -0
- data/cloudinary.gemspec +22 -0
- data/lib/cloudinary.rb +85 -0
- data/lib/cloudinary/blob.rb +11 -0
- data/lib/cloudinary/carrier_wave.rb +228 -0
- data/lib/cloudinary/downloader.rb +21 -0
- data/lib/cloudinary/helper.rb +53 -0
- data/lib/cloudinary/helpers.rb +7 -0
- data/lib/cloudinary/migrator.rb +361 -0
- data/lib/cloudinary/uploader.rb +114 -0
- data/lib/cloudinary/utils.rb +98 -0
- data/lib/cloudinary/version.rb +4 -0
- metadata +85 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
Cloudinary
|
2
|
+
==========
|
3
|
+
|
4
|
+
Cloudinary allows web applications to manage web resources in the cloud leveraging cloud-solutions.
|
5
|
+
Cloudinary offers a solution to the entire asset management workflow, from upload to transformations, optimizations, storage and delivery.
|
6
|
+
|
7
|
+
## Setup ######################################################################
|
8
|
+
|
9
|
+
To install the Cloudinary Ruby GEM, run:
|
10
|
+
|
11
|
+
$ gem install cloudinary
|
12
|
+
|
13
|
+
I you use Rails 3.x or higher, edit your Gemfile, add the following line and run 'bundle'
|
14
|
+
|
15
|
+
$ gem 'cloudinary'
|
16
|
+
|
17
|
+
Or in Rails 2.x, edit your environment.rb and add:
|
18
|
+
|
19
|
+
$ config.gem 'cloudinary'
|
20
|
+
|
21
|
+
If you would like to use our optional integration module of image uploads with ActiveRecord using CarrierWave, install CarrierWave to:
|
22
|
+
|
23
|
+
$ gem install carrierwave
|
24
|
+
|
25
|
+
Rails 3.x Gemfile:
|
26
|
+
|
27
|
+
$ gem 'carrierwave'
|
28
|
+
|
29
|
+
Rails 2.x environment.rb
|
30
|
+
|
31
|
+
$ config.gem 'carrierwave', :version => '~> 0.4.1'
|
32
|
+
|
33
|
+
|
34
|
+
## Usage ######################################################################
|
35
|
+
|
36
|
+
Refer to Cloudinary Documentation at:
|
37
|
+
http://cloudinary.com/documentation
|
38
|
+
|
39
|
+
|
40
|
+
For documetation about Ruby on Rails integration see:
|
41
|
+
http://cloudinary.com/documentation/rails_integration
|
42
|
+
|
43
|
+
|
44
|
+
## License #######################################################################
|
45
|
+
|
46
|
+
Released under the MIT license.
|
data/Rakefile
ADDED
data/cloudinary.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "cloudinary/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "cloudinary"
|
7
|
+
s.version = Cloudinary::VERSION
|
8
|
+
s.authors = ["Nadav Soferman","Itai Lahan","Tal Lev-Ami"]
|
9
|
+
s.email = ["nadav.soferman@cloudinary.com","itai.lahan@cloudinary.com","tal.levami@cloudinary.com"]
|
10
|
+
s.homepage = "http://cloudinary.com"
|
11
|
+
s.summary = %q{Client library for easily using the Cloudinary service}
|
12
|
+
s.description = %q{Client library for easily using the Cloudinary service}
|
13
|
+
|
14
|
+
s.rubyforge_project = "cloudinary"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency "rest-client"
|
22
|
+
end
|
data/lib/cloudinary.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# Copyright Cloudinary
|
2
|
+
require "ostruct"
|
3
|
+
require "cloudinary/version"
|
4
|
+
require "cloudinary/utils"
|
5
|
+
require "cloudinary/uploader"
|
6
|
+
require "cloudinary/downloader"
|
7
|
+
require "cloudinary/migrator"
|
8
|
+
require "cloudinary/blob"
|
9
|
+
require 'active_support'
|
10
|
+
if defined?(::CarrierWave)
|
11
|
+
require "cloudinary/carrier_wave"
|
12
|
+
end
|
13
|
+
|
14
|
+
if defined?(::ActionView::Base)
|
15
|
+
require "cloudinary/helper"
|
16
|
+
end
|
17
|
+
|
18
|
+
if !nil.respond_to?(:blank?)
|
19
|
+
class Object
|
20
|
+
def blank?
|
21
|
+
respond_to?(:empty?) ? empty? : !self
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class NilClass #:nodoc:
|
26
|
+
def blank?
|
27
|
+
true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class FalseClass #:nodoc:
|
32
|
+
def blank?
|
33
|
+
true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class TrueClass #:nodoc:
|
38
|
+
def blank?
|
39
|
+
false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Array #:nodoc:
|
44
|
+
alias_method :blank?, :empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
class Hash #:nodoc:
|
48
|
+
alias_method :blank?, :empty?
|
49
|
+
end
|
50
|
+
|
51
|
+
class String #:nodoc:
|
52
|
+
def blank?
|
53
|
+
self !~ /\S/
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class Numeric #:nodoc:
|
58
|
+
def blank?
|
59
|
+
false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module Cloudinary
|
65
|
+
@@config = nil
|
66
|
+
|
67
|
+
def self.config(new_config=nil)
|
68
|
+
@@config = new_config if new_config
|
69
|
+
if block_given?
|
70
|
+
@@config = OpenStruct.new
|
71
|
+
yield(@@config)
|
72
|
+
end
|
73
|
+
# Heroku support
|
74
|
+
if @@config.nil? && ENV["CLOUDINARY_CLOUD_NAME"]
|
75
|
+
@@config = OpenStruct.new(
|
76
|
+
"cloud_name" => ENV["CLOUDINARY_CLOUD_NAME"],
|
77
|
+
"api_key" => ENV["CLOUDINARY_API_KEY"],
|
78
|
+
"api_secret" => ENV["CLOUDINARY_API_SECRET"],
|
79
|
+
"secure_distribution" => ENV["CLOUDINARY_SECURE_DISTRIBUTION"],
|
80
|
+
"private_cdn" => ENV["CLOUDINARY_PRIVATE_CDN"].to_s == 'true'
|
81
|
+
)
|
82
|
+
end
|
83
|
+
@@config ||= OpenStruct.new((YAML.load_file(Rails.root.join("config").join("cloudinary.yml"))[Rails.env] rescue {}))
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# Copyright Cloudinary
|
2
|
+
class Cloudinary::Blob < StringIO
|
3
|
+
attr_reader :original_filename, :content_type
|
4
|
+
alias_method :path, :original_filename
|
5
|
+
|
6
|
+
def initialize(data, options={})
|
7
|
+
super(data)
|
8
|
+
@original_filename = options[:original_filename] || "cloudinaryfile"
|
9
|
+
@content_type = options[:content_type] || "application/octet-stream"
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
# Copyright Cloudinary
|
2
|
+
require 'pp'
|
3
|
+
module Cloudinary::CarrierWave
|
4
|
+
class UploadError < StandardError
|
5
|
+
attr_reader :http_code
|
6
|
+
def initialize(message, http_code)
|
7
|
+
super(message)
|
8
|
+
@http_code = http_code
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def eager
|
14
|
+
process :eager => true
|
15
|
+
end
|
16
|
+
|
17
|
+
def convert(format)
|
18
|
+
process :convert => format
|
19
|
+
end
|
20
|
+
|
21
|
+
def resize_to_limit(width, height)
|
22
|
+
process :resize_to_limit => [width, height]
|
23
|
+
end
|
24
|
+
|
25
|
+
def resize_to_fit(width, height)
|
26
|
+
process :resize_to_fit => [width, height]
|
27
|
+
end
|
28
|
+
|
29
|
+
def resize_to_fill(width, height, gravity="Center")
|
30
|
+
process :resize_to_fill => [width, height, gravity]
|
31
|
+
end
|
32
|
+
|
33
|
+
def resize_and_pad(width, height, background=:transparent, gravity="Center")
|
34
|
+
process :resize_and_pad => [width, height, background, gravity]
|
35
|
+
end
|
36
|
+
|
37
|
+
def scale(width, height)
|
38
|
+
process :scale => [width, height]
|
39
|
+
end
|
40
|
+
|
41
|
+
def crop(width, height, gravity="Center")
|
42
|
+
process :crop => [width, height, gravity]
|
43
|
+
end
|
44
|
+
|
45
|
+
def cloudinary_transformation(options)
|
46
|
+
process :cloudinary_transformation => options
|
47
|
+
end
|
48
|
+
|
49
|
+
def tags(*tags)
|
50
|
+
process :tags=>tags
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.included(base)
|
55
|
+
base.storage Cloudinary::CarrierWave::Storage
|
56
|
+
base.extend ClassMethods
|
57
|
+
base.send(:attr_accessor, :metadata)
|
58
|
+
end
|
59
|
+
|
60
|
+
def set_or_yell(hash, attr, value)
|
61
|
+
raise "conflicting transformation on #{attr} #{value}!=#{hash[attr]}" if hash[attr]
|
62
|
+
hash[attr] = value
|
63
|
+
end
|
64
|
+
|
65
|
+
def transformation
|
66
|
+
return @transformation if @transformation
|
67
|
+
transformation = {}
|
68
|
+
self.class.processors.each do
|
69
|
+
|name, args|
|
70
|
+
case name
|
71
|
+
when :convert # Do nothing. This is handled by format
|
72
|
+
when :resize_to_limit
|
73
|
+
set_or_yell(transformation, :width, args[0])
|
74
|
+
set_or_yell(transformation, :height, args[1])
|
75
|
+
set_or_yell(transformation, :crop, :limit)
|
76
|
+
when :resize_to_fit
|
77
|
+
set_or_yell(transformation, :width, args[0])
|
78
|
+
set_or_yell(transformation, :height, args[1])
|
79
|
+
set_or_yell(transformation, :crop, :fit)
|
80
|
+
when :resize_to_fill
|
81
|
+
set_or_yell(transformation, :width, args[0])
|
82
|
+
set_or_yell(transformation, :height, args[1])
|
83
|
+
set_or_yell(transformation, :gravity, args[2].to_s.downcase)
|
84
|
+
set_or_yell(transformation, :crop, :fill)
|
85
|
+
when :resize_to_pad
|
86
|
+
set_or_yell(transformation, :width, args[0])
|
87
|
+
set_or_yell(transformation, :height, args[1])
|
88
|
+
set_or_yell(transformation, :gravity, args[3].to_s.downcase)
|
89
|
+
set_or_yell(transformation, :crop, :pad)
|
90
|
+
when :scale
|
91
|
+
set_or_yell(transformation, :width, args[0])
|
92
|
+
set_or_yell(transformation, :height, args[1])
|
93
|
+
set_or_yell(transformation, :crop, :scale)
|
94
|
+
when :crop
|
95
|
+
set_or_yell(transformation, :width, args[0])
|
96
|
+
set_or_yell(transformation, :height, args[1])
|
97
|
+
set_or_yell(transformation, :gravity, args[2].to_s.downcase)
|
98
|
+
set_or_yell(transformation, :crop, :crop)
|
99
|
+
when :cloudinary_transformation
|
100
|
+
args.each do
|
101
|
+
|attr, value|
|
102
|
+
set_or_yell(transformation, attr, value)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
@transformation = transformation
|
107
|
+
@transformation
|
108
|
+
end
|
109
|
+
|
110
|
+
def eager
|
111
|
+
@eager ||= self.class.processors.any?{|processor| processor[0] == :eager}
|
112
|
+
end
|
113
|
+
|
114
|
+
def tags
|
115
|
+
@tags ||= self.class.processors.select{|processor| processor[0] == :tags}.map(&:last).first
|
116
|
+
end
|
117
|
+
|
118
|
+
def format
|
119
|
+
format_processor = self.class.processors.find{|processor| processor[0] == :convert}
|
120
|
+
if format_processor
|
121
|
+
if format_processor[1].is_a?(Array)
|
122
|
+
return format_processor[1][0]
|
123
|
+
end
|
124
|
+
return format_processor[1]
|
125
|
+
end
|
126
|
+
the_filename = original_filename || stored_filename
|
127
|
+
return the_filename.split(".").last if the_filename.include?(".")
|
128
|
+
"png" # TODO Default format?
|
129
|
+
end
|
130
|
+
|
131
|
+
def url(*args)
|
132
|
+
if(args.first)
|
133
|
+
super
|
134
|
+
else
|
135
|
+
options = self.class.version_names.blank? ? {} : self.transformation
|
136
|
+
Cloudinary::Utils.cloudinary_url(self.my_filename, options.clone)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def process!(new_file=nil)
|
141
|
+
# Do nothing
|
142
|
+
end
|
143
|
+
|
144
|
+
def stored_filename
|
145
|
+
@stored_filename ||= model.read_uploader(mounted_as)
|
146
|
+
end
|
147
|
+
|
148
|
+
def my_filename
|
149
|
+
@my_filename ||= stored_filename || ("#{self.public_id}.#{self.format}")
|
150
|
+
end
|
151
|
+
|
152
|
+
def public_id
|
153
|
+
return @public_id if @public_id
|
154
|
+
if stored_filename
|
155
|
+
last_dot = stored_filename.rindex(".")
|
156
|
+
@public_id = last_dot ? stored_filename[0, last_dot] : stored_filename
|
157
|
+
end
|
158
|
+
@public_id ||= Cloudinary::Utils.random_public_id
|
159
|
+
end
|
160
|
+
|
161
|
+
def download!(uri)
|
162
|
+
uri = process_uri(uri)
|
163
|
+
self.original_filename = @cache_id = @filename = File.basename(uri.path).gsub(/[^a-zA-Z0-9\.\-\+_]/, '')
|
164
|
+
@file = RemoteFile.new(uri, @filename)
|
165
|
+
end
|
166
|
+
|
167
|
+
def blank?
|
168
|
+
self.filename.blank? && self.stored_filename.blank?
|
169
|
+
end
|
170
|
+
|
171
|
+
class RemoteFile
|
172
|
+
attr_reader :uri, :original_filename
|
173
|
+
def initialize(uri, filename)
|
174
|
+
@uri = uri
|
175
|
+
@original_filename = filename
|
176
|
+
end
|
177
|
+
|
178
|
+
def delete
|
179
|
+
# Do nothing. This is a virtual file.
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
class Storage < ::CarrierWave::Storage::Abstract
|
184
|
+
def store!(file)
|
185
|
+
# Moved to identifier...
|
186
|
+
if uploader.class.version_names.blank?
|
187
|
+
# This is the toplevel, need to upload the actual file.
|
188
|
+
params = uploader.transformation.dup
|
189
|
+
params[:return_error] = true
|
190
|
+
params[:format] = uploader.format
|
191
|
+
params[:public_id] = uploader.public_id.split("/").last
|
192
|
+
params[:tags] = uploader.tags if uploader.tags
|
193
|
+
eager_versions = uploader.versions.values.select(&:eager)
|
194
|
+
params[:eager] = eager_versions.map{|version| [version.transformation, version.format]} if eager_versions.length > 0
|
195
|
+
|
196
|
+
data = nil
|
197
|
+
if (file.is_a?(RemoteFile))
|
198
|
+
data = file.uri.to_s
|
199
|
+
else
|
200
|
+
data = file.file
|
201
|
+
data.rewind if !file.is_path? && data.respond_to?(:rewind)
|
202
|
+
end
|
203
|
+
uploader.metadata = Cloudinary::Uploader.upload(data, params)
|
204
|
+
if uploader.metadata["error"]
|
205
|
+
raise UploadError.new(uploader.metadata["error"]["message"], uploader.metadata["error"]["http_code"])
|
206
|
+
end
|
207
|
+
|
208
|
+
if uploader.metadata["version"]
|
209
|
+
raise "Only ActiveRecord supported at the moment!" if !(uploader.model.class.respond_to?(:update_all) && uploader.model.class.respond_to?(:primary_key))
|
210
|
+
primary_key = uploader.model.class.primary_key.to_sym
|
211
|
+
uploader.model.class.update_all(["#{uploader.mounted_as}=?", "v#{uploader.metadata["version"]}/#{identifier.split("/").last}"], {primary_key=>uploader.model.send(primary_key)})
|
212
|
+
end
|
213
|
+
# Will throw an exception on error
|
214
|
+
else
|
215
|
+
raise "nested versions are not allowed." if (uploader.class.version_names.length > 1)
|
216
|
+
# Do nothing
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def retrieve!(identifier)
|
221
|
+
# Do nothing
|
222
|
+
end
|
223
|
+
|
224
|
+
def identifier
|
225
|
+
(uploader.filename || uploader.stored_filename) ? uploader.my_filename : nil
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Copyright Cloudinary
|
2
|
+
class Cloudinary::Downloader
|
3
|
+
|
4
|
+
def self.download(source, options={})
|
5
|
+
options = options.clone
|
6
|
+
if !source.match(/^https?:\/\//i)
|
7
|
+
source = Cloudinary::Utils.cloudinary_url(source, options)
|
8
|
+
end
|
9
|
+
|
10
|
+
url = URI.parse(source)
|
11
|
+
http = Net::HTTP.new(url.host, url.port)
|
12
|
+
req = Net::HTTP::Get.new(url.path)
|
13
|
+
if url.port == 443
|
14
|
+
http.use_ssl=true
|
15
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
16
|
+
end
|
17
|
+
res = http.start{|agent| agent.request(req)}
|
18
|
+
return res.body
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# Copyright Cloudinary
|
2
|
+
|
3
|
+
module CloudinaryHelper
|
4
|
+
# Examples
|
5
|
+
# cl_image_tag "israel.png", :width=>100, :height=>100, :alt=>"hello" # W/H are not sent to cloudinary
|
6
|
+
# cl_image_tag "israel.png", :width=>100, :height=>100, :alt=>"hello", :crop=>:fit # W/H are sent to cloudinary
|
7
|
+
def cl_image_tag(source, options = {})
|
8
|
+
options = options.clone
|
9
|
+
source = cloudinary_url(source, options)
|
10
|
+
options[:width] = options.delete(:html_width) if options.include?(:html_width)
|
11
|
+
options[:height] = options.delete(:html_height) if options.include?(:html_height)
|
12
|
+
|
13
|
+
image_tag(source, options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def facebook_profile_image_tag(profile, options = {})
|
17
|
+
cl_image_tag(profile, {:type=>:facebook}.merge(options))
|
18
|
+
end
|
19
|
+
|
20
|
+
def twitter_profile_image_tag(profile, options = {})
|
21
|
+
cl_image_tag(profile, {:type=>:twitter}.merge(options))
|
22
|
+
end
|
23
|
+
|
24
|
+
def twitter_name_profile_image_tag(profile, options = {})
|
25
|
+
cl_image_tag(profile, {:type=>:twitter_name}.merge(options))
|
26
|
+
end
|
27
|
+
|
28
|
+
def cl_sprite_url(source, options = {})
|
29
|
+
options = options.clone
|
30
|
+
|
31
|
+
version_store = options.delete(:version_store)
|
32
|
+
if options[:version].blank? && (version_store == :file) && defined?(Rails) && defined?(Rails.root)
|
33
|
+
file_name = "#{Rails.root}/tmp/cloudinary/cloudinary_sprite_#{source.sub(/\..*/, '')}.version"
|
34
|
+
if File.exists?(file_name)
|
35
|
+
options[:version] = File.read(file_name).chomp
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
options[:format] = "css" unless source.ends_with?(".css")
|
40
|
+
cloudinary_url(source, options.merge(:type=>:sprite))
|
41
|
+
end
|
42
|
+
|
43
|
+
def cl_sprite_tag(source, options = {})
|
44
|
+
stylesheet_link_tag(cl_sprite_url(source, options))
|
45
|
+
end
|
46
|
+
|
47
|
+
def cloudinary_url(source, options = {})
|
48
|
+
options[:secure] = request.ssl? if !options.include?(:secure) && defined?(request) && request && request.respond_to?(:ssl?)
|
49
|
+
Cloudinary::Utils.cloudinary_url(source, options)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
ActionView::Base.send :include, CloudinaryHelper
|
@@ -0,0 +1,361 @@
|
|
1
|
+
# Copyright Cloudinary
|
2
|
+
|
3
|
+
class Cloudinary::Migrator
|
4
|
+
attr_reader :retrieve, :complete
|
5
|
+
attr_accessor :terminate, :in_process
|
6
|
+
attr_reader :db
|
7
|
+
attr_reader :work, :results, :mutex
|
8
|
+
attr_reader :extra_options
|
9
|
+
|
10
|
+
|
11
|
+
@@init = false
|
12
|
+
def self.init
|
13
|
+
return if @@init
|
14
|
+
@@init = true
|
15
|
+
|
16
|
+
begin
|
17
|
+
require 'sqlite3'
|
18
|
+
rescue LoadError
|
19
|
+
raise "Please add sqlite3 to your Gemfile"
|
20
|
+
end
|
21
|
+
require 'tempfile'
|
22
|
+
end
|
23
|
+
|
24
|
+
def json_decode(str)
|
25
|
+
Cloudinary::Utils.json_decode(str)
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(options={})
|
29
|
+
self.class.init
|
30
|
+
|
31
|
+
options[:db_file] = "tmp/migration#{$$}.db" if options[:private_database] && !options[:db_file]
|
32
|
+
@dbfile = options[:db_file] || "tmp/migration.db"
|
33
|
+
FileUtils.mkdir_p(File.dirname(@dbfile))
|
34
|
+
@db = SQLite3::Database.new @dbfile, :results_as_hash=>true
|
35
|
+
@retrieve = options[:retrieve]
|
36
|
+
@complete = options[:complete]
|
37
|
+
@debug = options[:debug] || false
|
38
|
+
@ignore_duplicates = options[:ignore_duplicates]
|
39
|
+
@threads = [options[:threads] || 10, 100].min
|
40
|
+
@extra_options = {:api_key=>options[:api_key], :api_secret=>options[:api_secret]}
|
41
|
+
@delete_after_done = options[:delete_after_done] || options[:private_database]
|
42
|
+
@max_processing = @threads * 10
|
43
|
+
@in_process = 0
|
44
|
+
@work = Queue.new
|
45
|
+
@results = Queue.new
|
46
|
+
@mutex = Mutex.new
|
47
|
+
@db.execute "
|
48
|
+
create table if not exists queue (
|
49
|
+
id integer primary key,
|
50
|
+
internal_id integer,
|
51
|
+
public_id text,
|
52
|
+
url text,
|
53
|
+
metadata text,
|
54
|
+
result string,
|
55
|
+
status text,
|
56
|
+
updated_at integer
|
57
|
+
)
|
58
|
+
"
|
59
|
+
@db.execute "
|
60
|
+
create index if not exists status_idx on queue (
|
61
|
+
status
|
62
|
+
)
|
63
|
+
"
|
64
|
+
@db.execute "
|
65
|
+
create unique index if not exists internal_id_idx on queue (
|
66
|
+
internal_id
|
67
|
+
)
|
68
|
+
"
|
69
|
+
@db.execute "
|
70
|
+
create unique index if not exists public_id_idx on queue (
|
71
|
+
public_id
|
72
|
+
)
|
73
|
+
"
|
74
|
+
if options[:reset_queue]
|
75
|
+
@db.execute("delete from queue")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def register_retrieve(&block)
|
80
|
+
@retrieve = block
|
81
|
+
end
|
82
|
+
|
83
|
+
def register_complete(&block)
|
84
|
+
@complete = block
|
85
|
+
end
|
86
|
+
|
87
|
+
def process(options={})
|
88
|
+
raise "url not given and no retieve callback given" if options[:url].nil? && self.retrieve.nil?
|
89
|
+
raise "id not given and retieve or complete callback given" if options[:id].nil? && (!self.retrieve.nil? || !self.complete.nil?)
|
90
|
+
|
91
|
+
debug("Process: #{options.inspect}")
|
92
|
+
start
|
93
|
+
process_results
|
94
|
+
wait_for_queue
|
95
|
+
options = options.dup
|
96
|
+
id = options.delete(:id)
|
97
|
+
url = options.delete(:url)
|
98
|
+
public_id = options.delete(:public_id)
|
99
|
+
row = {
|
100
|
+
"internal_id"=>id,
|
101
|
+
"url"=>url,
|
102
|
+
"public_id"=>public_id,
|
103
|
+
"metadata"=>options.to_json,
|
104
|
+
"status"=>"processing"
|
105
|
+
}
|
106
|
+
begin
|
107
|
+
insert_row(row)
|
108
|
+
add_to_work_queue(row)
|
109
|
+
rescue SQLite3::ConstraintException
|
110
|
+
raise if !@ignore_duplicates
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def done
|
115
|
+
start
|
116
|
+
process_all_pending
|
117
|
+
@terminate = true
|
118
|
+
1.upto(@threads){self.work << nil} # enough objects to release all waiting threads
|
119
|
+
@started = false
|
120
|
+
@db.close
|
121
|
+
File.delete(@dbfile) if @delete_after_done
|
122
|
+
end
|
123
|
+
|
124
|
+
def max_given_id
|
125
|
+
db.get_first_value("select max(internal_id) from queue").to_i
|
126
|
+
end
|
127
|
+
|
128
|
+
def close_if_needed(file)
|
129
|
+
if file.nil?
|
130
|
+
# Do nothing.
|
131
|
+
elsif file.respond_to?(:close!)
|
132
|
+
file.close!
|
133
|
+
elsif file.respond_to?(:close)
|
134
|
+
file.close
|
135
|
+
end
|
136
|
+
rescue
|
137
|
+
# Ignore errors in closing files
|
138
|
+
end
|
139
|
+
|
140
|
+
def temporary_file(data, filename)
|
141
|
+
file = RUBY_VERSION == "1.8.7" ? Tempfile.new('cloudinary') : Tempfile.new('cloudinary', :encoding => 'ascii-8bit')
|
142
|
+
file.unlink
|
143
|
+
file.write(data)
|
144
|
+
file.rewind
|
145
|
+
# Tempfile return path == nil after unlink, which break rest-client
|
146
|
+
class << file
|
147
|
+
attr_accessor :original_filename
|
148
|
+
def content_type
|
149
|
+
"application/octet-stream"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
file.original_filename = filename
|
153
|
+
file
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
def update_all(values)
|
159
|
+
@db.execute("update queue set #{values.keys.map{|key| "#{key}=?"}.join(",")}", *values.values)
|
160
|
+
end
|
161
|
+
|
162
|
+
def update_row(row, values)
|
163
|
+
values.merge!("updated_at"=>Time.now.to_i)
|
164
|
+
query = ["update queue set #{values.keys.map{|key| "#{key}=?"}.join(",")} where id=?"] + values.values + [row["id"]]
|
165
|
+
result = @db.execute(*query)
|
166
|
+
values.each{|key, value| row[key.to_s] = value}
|
167
|
+
row
|
168
|
+
end
|
169
|
+
|
170
|
+
def insert_row(values)
|
171
|
+
values.merge!("updated_at"=>Time.now.to_i)
|
172
|
+
@db.execute("insert into queue (#{values.keys.join(",")}) values (#{values.keys.map{"?"}.join(",")})", *values.values)
|
173
|
+
values["id"] = @db.last_insert_row_id
|
174
|
+
end
|
175
|
+
|
176
|
+
def refill_queue(last_id)
|
177
|
+
@db.execute("select * from queue where status in ('error', 'processing') and id > ? limit ?", last_id, 10000) do
|
178
|
+
|row|
|
179
|
+
last_id = row["id"] if row["id"] > last_id
|
180
|
+
wait_for_queue
|
181
|
+
add_to_work_queue(row)
|
182
|
+
end
|
183
|
+
last_id
|
184
|
+
end
|
185
|
+
|
186
|
+
def process_results
|
187
|
+
while self.results.length > 0
|
188
|
+
row = self.results.pop
|
189
|
+
result = json_decode(row["result"])
|
190
|
+
debug("Done ID=#{row['internal_id']}, result=#{result.inspect}")
|
191
|
+
complete.call(row["internal_id"], result) if complete
|
192
|
+
if result["error"]
|
193
|
+
status = case result["error"]["http_code"]
|
194
|
+
when 400, 404 then "fatal" # Problematic request. Not a server problem.
|
195
|
+
else "error"
|
196
|
+
end
|
197
|
+
else
|
198
|
+
status = "completed"
|
199
|
+
end
|
200
|
+
updates = {:status=>status, :result=>row["result"]}
|
201
|
+
updates["public_id"] = result["public_id"] if result["public_id"] && !row["public_id"]
|
202
|
+
begin
|
203
|
+
update_row(row, updates)
|
204
|
+
rescue SQLite3::ConstraintException
|
205
|
+
updates = {:status=>"error", :result=>{:error=>{:message=>"public_id already exists"}}.to_json}
|
206
|
+
update_row(row, updates)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def try_try_again(tries=5)
|
212
|
+
retry_count = 0
|
213
|
+
begin
|
214
|
+
return yield
|
215
|
+
rescue
|
216
|
+
retry_count++
|
217
|
+
raise if retry_count > tries
|
218
|
+
sleep rand * 3
|
219
|
+
retry
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def start
|
224
|
+
return if @started
|
225
|
+
@started = true
|
226
|
+
@terminate = false
|
227
|
+
|
228
|
+
self.work.clear
|
229
|
+
|
230
|
+
main = self
|
231
|
+
Thread.abort_on_exception = true
|
232
|
+
1.upto(@threads) do
|
233
|
+
|i|
|
234
|
+
Thread.start do
|
235
|
+
while !main.terminate
|
236
|
+
file = nil
|
237
|
+
row = main.work.pop
|
238
|
+
next if row.nil?
|
239
|
+
begin
|
240
|
+
debug "Thread #{i} - processing row #{row.inspect}. #{main.work.length} work waiting. #{main.results.length} results waiting."
|
241
|
+
url = row["url"]
|
242
|
+
cw = false
|
243
|
+
result = nil
|
244
|
+
if url.nil? && !self.retrieve.nil?
|
245
|
+
data = self.retrieve.call(row["internal_id"])
|
246
|
+
if defined?(ActiveRecord::Base) && data.is_a?(ActiveRecord::Base)
|
247
|
+
cw = true
|
248
|
+
data.save!
|
249
|
+
elsif defined?(Cloudinary::CarrierWave) && data.is_a?(Cloudinary::CarrierWave)
|
250
|
+
cw = true
|
251
|
+
begin
|
252
|
+
data.model.save!
|
253
|
+
rescue Cloudinary::CarrierWave::UploadError
|
254
|
+
# upload errors will be handled by the result values.
|
255
|
+
end
|
256
|
+
result = data.metadata
|
257
|
+
elsif data.respond_to?(:read) && data.respond_to?(:path)
|
258
|
+
# This is an IO style object, pass as is.
|
259
|
+
file = data
|
260
|
+
elsif !data.nil?
|
261
|
+
file = main.temporary_file(data, row["public_id"] || "cloudinaryfile")
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
if url || file
|
266
|
+
options = main.extra_options.merge(:public_id=>row["public_id"])
|
267
|
+
json_decode(row["metadata"]).each do
|
268
|
+
|key, value|
|
269
|
+
options[key.to_sym] = value
|
270
|
+
end
|
271
|
+
|
272
|
+
result = Cloudinary::Uploader.upload(url || file, options.merge(:return_error=>true)) || ({:error=>{:message=>"Received nil from uploader!"}})
|
273
|
+
elsif cw
|
274
|
+
result ||= {"status" => "saved"}
|
275
|
+
else
|
276
|
+
result = {"error" => {"message" => "Empty data and url", "http_code"=>404}}
|
277
|
+
end
|
278
|
+
main.results << {"id"=>row["id"], "internal_id"=>row["internal_id"], "result"=>result.to_json}
|
279
|
+
rescue => e
|
280
|
+
$stderr.print "Thread #{i} - Error in processing row #{row.inspect} - #{e}\n"
|
281
|
+
debug(e.backtrace.join("\n"))
|
282
|
+
sleep 1
|
283
|
+
ensure
|
284
|
+
main.mutex.synchronize{main.in_process -= 1}
|
285
|
+
main.close_if_needed(file)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
retry_previous_queue # Retry all work from previous iteration before we start processing this one.
|
292
|
+
end
|
293
|
+
|
294
|
+
def debug(message)
|
295
|
+
if @debug
|
296
|
+
$stderr.print "#{Time.now} Cloudinary::Migrator #{message}\n"
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def retry_previous_queue
|
301
|
+
last_id = 0
|
302
|
+
begin
|
303
|
+
prev_last_id, last_id = last_id, refill_queue(last_id)
|
304
|
+
end while last_id > prev_last_id
|
305
|
+
process_results
|
306
|
+
end
|
307
|
+
|
308
|
+
def process_all_pending
|
309
|
+
# Waiting for work to finish. While we are at it, process results.
|
310
|
+
while self.in_process > 0
|
311
|
+
process_results
|
312
|
+
sleep 0.1
|
313
|
+
end
|
314
|
+
# Make sure we processed all the results
|
315
|
+
process_results
|
316
|
+
end
|
317
|
+
|
318
|
+
def add_to_work_queue(row)
|
319
|
+
self.work << row
|
320
|
+
mutex.synchronize{self.in_process += 1}
|
321
|
+
end
|
322
|
+
|
323
|
+
def wait_for_queue
|
324
|
+
# Waiting f
|
325
|
+
while self.work.length > @max_processing
|
326
|
+
process_results
|
327
|
+
sleep 0.1
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def self.sample
|
332
|
+
migrator = Cloudinary::Migrator.new(
|
333
|
+
:retrieve=>proc{|id| Post.find(id).data},
|
334
|
+
:complete=>proc{|id, result| a}
|
335
|
+
)
|
336
|
+
|
337
|
+
Post.find_each(:conditions=>["id > ?", migrator.max_given_id], :select=>"id") do
|
338
|
+
|post|
|
339
|
+
migrator.process(:id=>post.id, :public_id=>"post_#{post.id}")
|
340
|
+
end
|
341
|
+
migrator.done
|
342
|
+
end
|
343
|
+
|
344
|
+
def self.test
|
345
|
+
posts = {}
|
346
|
+
done = {}
|
347
|
+
migrator = Cloudinary::Migrator.new(
|
348
|
+
:retrieve=>proc{|id| posts[id]},
|
349
|
+
:complete=>proc{|id, result| $stderr.print "done #{id} #{result}\n"; done[id] = result}
|
350
|
+
)
|
351
|
+
start = migrator.max_given_id + 1
|
352
|
+
(start..1000).each{|i| posts[i] = "hello#{i}"}
|
353
|
+
|
354
|
+
posts.each do
|
355
|
+
|id, data|
|
356
|
+
migrator.process(:id=>id, :public_id=>"post_#{id}")
|
357
|
+
end
|
358
|
+
migrator.done
|
359
|
+
pp [done.length, start]
|
360
|
+
end
|
361
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# Copyright Cloudinary
|
2
|
+
require 'rest_client'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
class Cloudinary::Uploader
|
6
|
+
|
7
|
+
def self.upload(file, options={})
|
8
|
+
call_api("upload", options) do
|
9
|
+
params = {:timestamp=>Time.now.to_i,
|
10
|
+
:transformation => Cloudinary::Utils.generate_transformation_string(options),
|
11
|
+
:public_id=> options[:public_id],
|
12
|
+
:format=>options[:format],
|
13
|
+
:tags=>options[:tags] && Array(options[:tags]).join(",")}.reject{|k,v| v.blank?}
|
14
|
+
if options[:eager]
|
15
|
+
params[:eager] = options[:eager].map do
|
16
|
+
|transformation|
|
17
|
+
transformation, format = Array(transformation) # format is optional
|
18
|
+
[Cloudinary::Utils.generate_transformation_string(transformation.clone), format].compact.join("/")
|
19
|
+
end.join("|")
|
20
|
+
end
|
21
|
+
if file.respond_to?(:read) || file =~ /^https?:/
|
22
|
+
params[:file] = file
|
23
|
+
else
|
24
|
+
params[:file] = File.open(file, "rb")
|
25
|
+
end
|
26
|
+
[params, [:file]]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.generate_sprite(tag, options={})
|
31
|
+
version_store = options.delete(:version_store)
|
32
|
+
|
33
|
+
result = call_api("sprite", options) do
|
34
|
+
{
|
35
|
+
:timestamp=>Time.now.to_i,
|
36
|
+
:tag=>tag,
|
37
|
+
:transformation => Cloudinary::Utils.generate_transformation_string(options)
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
if version_store == :file && result && result["version"]
|
42
|
+
if defined?(Rails) && defined?(Rails.root)
|
43
|
+
FileUtils.mkdir_p("#{Rails.root}/tmp/cloudinary")
|
44
|
+
File.open("#{Rails.root}/tmp/cloudinary/cloudinary_sprite_#{tag}.version", "w"){|file| file.print result["version"].to_s}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
return result
|
48
|
+
end
|
49
|
+
|
50
|
+
# options may include 'exclusive' (boolean) which causes clearing this tag from all other resources
|
51
|
+
def self.add_tag(tag, public_ids = [], options = {})
|
52
|
+
exclusive = options.delete(:exclusive)
|
53
|
+
command = exclusive ? "set_exclusive" : "add"
|
54
|
+
return self.call_tags_api(tag, command, public_ids, options)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.remove_tag(tag, public_ids = [], options = {})
|
58
|
+
return self.call_tags_api(tag, "remove", public_ids, options)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.replace_tag(tag, public_ids = [], options = {})
|
62
|
+
return self.call_tags_api(tag, "replace", public_ids, options)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def self.call_tags_api(tag, command, public_ids = [], options = {})
|
68
|
+
return call_api("tags", options) do
|
69
|
+
{
|
70
|
+
:timestamp=>Time.now.to_i,
|
71
|
+
:tag=>tag,
|
72
|
+
:public_ids => Array(public_ids),
|
73
|
+
:command => command
|
74
|
+
}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.call_api(action, options)
|
79
|
+
options = options.clone
|
80
|
+
return_error = options.delete(:return_error)
|
81
|
+
api_key = options[:api_key] || Cloudinary.config.api_key || raise("Must supply api_key")
|
82
|
+
api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise("Must supply api_secret")
|
83
|
+
|
84
|
+
params, non_signable = yield
|
85
|
+
non_signable ||= []
|
86
|
+
|
87
|
+
params[:signature] = Cloudinary::Utils.api_sign_request(params.reject{|k,v| non_signable.include?(k)}, api_secret)
|
88
|
+
params[:api_key] = api_key
|
89
|
+
cloudinary = options.delete(:upload_prefix) || Cloudinary.config.upload_prefix || "https://api.cloudinary.com"
|
90
|
+
|
91
|
+
resource_type = options.delete(:resource_type) || "image"
|
92
|
+
result = nil
|
93
|
+
cloud_name = Cloudinary.config.cloud_name || raise("Must supply cloud_name")
|
94
|
+
RestClient::Request.execute(:method => :post, :url => "#{cloudinary}/v1_1/#{cloud_name}/#{resource_type}/#{action}", :payload => params, :timeout=>60) do
|
95
|
+
|response, request, tmpresult|
|
96
|
+
raise "Server returned unexpected status code - #{response.code} - #{response.body}" if ![200,400,500].include?(response.code)
|
97
|
+
begin
|
98
|
+
result = Cloudinary::Utils.json_decode(response.body)
|
99
|
+
rescue => e
|
100
|
+
# Error is parsing json
|
101
|
+
raise "Error parsing server response (#{response.code}) - #{response.body}. Got - #{e}"
|
102
|
+
end
|
103
|
+
if result["error"]
|
104
|
+
if return_error
|
105
|
+
result["error"]["http_code"] = response.code
|
106
|
+
else
|
107
|
+
raise result["error"]["message"]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
result
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# Copyright Cloudinary
|
2
|
+
require 'digest/sha1'
|
3
|
+
|
4
|
+
class Cloudinary::Utils
|
5
|
+
SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net"
|
6
|
+
|
7
|
+
# Warning: options are being destructively updated!
|
8
|
+
def self.generate_transformation_string(options={})
|
9
|
+
width = options[:width]
|
10
|
+
height = options[:height]
|
11
|
+
size = options.delete(:size)
|
12
|
+
width, height = size.split("x") if size
|
13
|
+
options.delete(:width) if width && width < 1
|
14
|
+
options.delete(:height) if height && height < 1
|
15
|
+
|
16
|
+
crop = options.delete(:crop)
|
17
|
+
width=height=nil if crop.nil?
|
18
|
+
|
19
|
+
gravity = options.delete(:gravity)
|
20
|
+
quality = options.delete(:quality)
|
21
|
+
named_transformation = Array(options.delete(:transformation)).join(".")
|
22
|
+
prefix = options.delete(:prefix)
|
23
|
+
|
24
|
+
params = {:w=>width, :h=>height, :t=>named_transformation, :c=>crop, :q=>quality, :g=>gravity, :p=>prefix}
|
25
|
+
transformation = params.reject{|k,v| v.blank?}.map{|k,v| [k.to_s, v]}.sort_by(&:first).map{|k,v| "#{k}_#{v}"}.join(",")
|
26
|
+
raw_transformation = options.delete(:raw_transformation)
|
27
|
+
transformation = [transformation, raw_transformation].reject(&:blank?).join(",")
|
28
|
+
transformation
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.api_sign_request(params_to_sign, api_secret)
|
32
|
+
to_sign = params_to_sign.reject{|k,v| v.blank?}.map{|k,v| [k.to_s, v.is_a?(Array) ? v.join(",") : v]}.sort_by(&:first).map{|k,v| "#{k}=#{v}"}.join("&")
|
33
|
+
Digest::SHA1.hexdigest("#{to_sign}#{api_secret}")
|
34
|
+
end
|
35
|
+
|
36
|
+
# Warning: options are being destructively updated!
|
37
|
+
def self.cloudinary_url(source, options = {})
|
38
|
+
transformation = self.generate_transformation_string(options)
|
39
|
+
|
40
|
+
type = options.delete(:type) || :upload
|
41
|
+
resource_type = options.delete(:resource_type) || "image"
|
42
|
+
version = options.delete(:version)
|
43
|
+
|
44
|
+
format = options.delete(:format)
|
45
|
+
source = "#{source}.#{format}" if format
|
46
|
+
|
47
|
+
# Configuration options
|
48
|
+
# newsodrome.cloudinary.com, images.newsodrome.com, cloudinary.com/res/newsodrome, a9fj209daf.cloudfront.net
|
49
|
+
cloud_name = options.delete(:cloud_name) || Cloudinary.config.cloud_name || raise("Must supply cloud_name in tag or in configuration")
|
50
|
+
|
51
|
+
if cloud_name.start_with?("/")
|
52
|
+
prefix = "/res" + cloud_name
|
53
|
+
else
|
54
|
+
secure = options.delete(:secure) || Cloudinary.config.secure
|
55
|
+
private_cdn = options.delete(:private_cdn) || Cloudinary.config.private_cdn
|
56
|
+
secure_distribution = options.delete(:secure_distribution) || Cloudinary.config.secure_distribution
|
57
|
+
if secure && secure_distribution.nil?
|
58
|
+
if private_cdn
|
59
|
+
raise "secure_distribution not defined"
|
60
|
+
else
|
61
|
+
secure_distribution = SHARED_CDN
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
if secure
|
66
|
+
prefix = "https://#{secure_distribution}"
|
67
|
+
else
|
68
|
+
prefix = "http://#{private_cdn ? "#{cloud_name}-" : ""}res.cloudinary.com"
|
69
|
+
end
|
70
|
+
prefix += "/#{cloud_name}" if !private_cdn
|
71
|
+
end
|
72
|
+
|
73
|
+
source = prefix + "/" + [resource_type,
|
74
|
+
type, transformation, version ? "v#{version}" : nil,
|
75
|
+
source].reject(&:blank?).join("/").gsub("//", "/")
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.random_public_id
|
79
|
+
(defined?(ActiveSupport::SecureRandom) ? ActiveSupport::SecureRandom : SecureRandom).base64(16).downcase.gsub(/[^a-z0-9]/, "")
|
80
|
+
end
|
81
|
+
|
82
|
+
@@json_decode = false
|
83
|
+
def self.json_decode(str)
|
84
|
+
if !@@json_decode
|
85
|
+
@@json_decode = true
|
86
|
+
begin
|
87
|
+
require 'json'
|
88
|
+
rescue LoadError
|
89
|
+
begin
|
90
|
+
require 'active_support/json'
|
91
|
+
rescue LoadError
|
92
|
+
raise "Please add the json gem or active_support to your Gemfile"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
defined?(JSON) ? JSON.parse(str) : ActiveSupport::JSON.decode(str)
|
97
|
+
end
|
98
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cloudinary
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 1.0.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Nadav Soferman
|
9
|
+
- Itai Lahan
|
10
|
+
- Tal Lev-Ami
|
11
|
+
autorequire:
|
12
|
+
bindir: bin
|
13
|
+
cert_chain: []
|
14
|
+
|
15
|
+
date: 2012-02-22 00:00:00 +02:00
|
16
|
+
default_executable:
|
17
|
+
dependencies:
|
18
|
+
- !ruby/object:Gem::Dependency
|
19
|
+
name: rest-client
|
20
|
+
prerelease: false
|
21
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
22
|
+
none: false
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: "0"
|
27
|
+
type: :runtime
|
28
|
+
version_requirements: *id001
|
29
|
+
description: Client library for easily using the Cloudinary service
|
30
|
+
email:
|
31
|
+
- nadav.soferman@cloudinary.com
|
32
|
+
- itai.lahan@cloudinary.com
|
33
|
+
- tal.levami@cloudinary.com
|
34
|
+
executables: []
|
35
|
+
|
36
|
+
extensions: []
|
37
|
+
|
38
|
+
extra_rdoc_files: []
|
39
|
+
|
40
|
+
files:
|
41
|
+
- .gitignore
|
42
|
+
- Gemfile
|
43
|
+
- README.md
|
44
|
+
- Rakefile
|
45
|
+
- cloudinary.gemspec
|
46
|
+
- lib/cloudinary.rb
|
47
|
+
- lib/cloudinary/blob.rb
|
48
|
+
- lib/cloudinary/carrier_wave.rb
|
49
|
+
- lib/cloudinary/downloader.rb
|
50
|
+
- lib/cloudinary/helper.rb
|
51
|
+
- lib/cloudinary/helpers.rb
|
52
|
+
- lib/cloudinary/migrator.rb
|
53
|
+
- lib/cloudinary/uploader.rb
|
54
|
+
- lib/cloudinary/utils.rb
|
55
|
+
- lib/cloudinary/version.rb
|
56
|
+
has_rdoc: true
|
57
|
+
homepage: http://cloudinary.com
|
58
|
+
licenses: []
|
59
|
+
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: "0"
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: "0"
|
77
|
+
requirements: []
|
78
|
+
|
79
|
+
rubyforge_project: cloudinary
|
80
|
+
rubygems_version: 1.6.2
|
81
|
+
signing_key:
|
82
|
+
specification_version: 3
|
83
|
+
summary: Client library for easily using the Cloudinary service
|
84
|
+
test_files: []
|
85
|
+
|