haravan_theme 0.0.25
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.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +31 -0
- data/CONTRIBUTING +6 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.md +171 -0
- data/Rakefile +28 -0
- data/bin/theme +26 -0
- data/doc/API-key-and-password.jpg +0 -0
- data/doc/how_to_find_theme_id.png +0 -0
- data/haravan_theme.gemspec +33 -0
- data/lib/certs/cacert.pem +3988 -0
- data/lib/haravan_theme/api_checker.rb +41 -0
- data/lib/haravan_theme/cli.rb +344 -0
- data/lib/haravan_theme/file_filters.rb +18 -0
- data/lib/haravan_theme/filters/blacklist.rb +17 -0
- data/lib/haravan_theme/filters/command_input.rb +17 -0
- data/lib/haravan_theme/filters/whitelist.rb +19 -0
- data/lib/haravan_theme/releases.rb +57 -0
- data/lib/haravan_theme/version.rb +3 -0
- data/lib/haravan_theme.rb +171 -0
- data/shipit.rubygems.yml +1 -0
- data/spec/cassettes/api_check_api_down.yml +55 -0
- data/spec/cassettes/api_check_success.yml +55 -0
- data/spec/cassettes/api_check_unauthorized.yml +55 -0
- data/spec/cassettes/timber_releases.yml +994 -0
- data/spec/smoke/ca_cert_spec.rb +43 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/unit/api_checker_spec.rb +44 -0
- data/spec/unit/cli_spec.rb +90 -0
- data/spec/unit/file_filters_spec.rb +37 -0
- data/spec/unit/filters/blacklist_spec.rb +32 -0
- data/spec/unit/filters/command_input_spec.rb +18 -0
- data/spec/unit/filters/whitelist_spec.rb +41 -0
- data/spec/unit/releases_spec.rb +45 -0
- metadata +236 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
module HaravanTheme
|
2
|
+
class APIChecker
|
3
|
+
class APIResult
|
4
|
+
OK = 200
|
5
|
+
UNAUTHORIZED = 401
|
6
|
+
SERVER_ERROR_CODES = (500..599)
|
7
|
+
|
8
|
+
attr_reader :response
|
9
|
+
def initialize(http_response)
|
10
|
+
@response = http_response
|
11
|
+
end
|
12
|
+
|
13
|
+
def accessed_api?
|
14
|
+
response.code == OK
|
15
|
+
end
|
16
|
+
|
17
|
+
def cannot_access_api?
|
18
|
+
!accessed_api?
|
19
|
+
end
|
20
|
+
|
21
|
+
def invalid_config?
|
22
|
+
response.code == UNAUTHORIZED
|
23
|
+
end
|
24
|
+
|
25
|
+
def api_down?
|
26
|
+
SERVER_ERROR_CODES.include?(response.code)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(client)
|
31
|
+
@client = client
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_connectivity
|
35
|
+
return APIResult.new(client.get_index)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
attr_reader :client
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,344 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'yaml'
|
3
|
+
YAML::ENGINE.yamler = 'syck' if defined? Syck
|
4
|
+
require 'abbrev'
|
5
|
+
require 'base64'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'json'
|
8
|
+
require 'filewatcher'
|
9
|
+
require 'launchy'
|
10
|
+
require 'mimemagic'
|
11
|
+
require 'haravan_theme/file_filters'
|
12
|
+
|
13
|
+
module HaravanTheme
|
14
|
+
EXTENSIONS = [
|
15
|
+
{mimetype: 'application/x-liquid', extensions: %w(liquid), parents: 'text/plain'},
|
16
|
+
{mimetype: 'application/json', extensions: %w(json), parents: 'text/plain'},
|
17
|
+
{mimetype: 'application/js', extensions: %w(map), parents: 'text/plain'},
|
18
|
+
{mimetype: 'application/vnd.ms-fontobject', extensions: %w(eot)},
|
19
|
+
{mimetype: 'image/svg+xml', extensions: %w(svg svgz)}
|
20
|
+
]
|
21
|
+
|
22
|
+
def self.configureMimeMagic
|
23
|
+
HaravanTheme::EXTENSIONS.each do |extension|
|
24
|
+
MimeMagic.add(extension.delete(:mimetype), extension)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Cli < Thor
|
29
|
+
include Thor::Actions
|
30
|
+
|
31
|
+
IGNORE = %w(config.yml)
|
32
|
+
DEFAULT_WHITELIST = %w(layout/ assets/ config/ snippets/ templates/ locales/)
|
33
|
+
TIMEFORMAT = "%H:%M:%S"
|
34
|
+
|
35
|
+
tasks.keys.abbrev.each do |shortcut, command|
|
36
|
+
map shortcut => command.to_sym
|
37
|
+
end
|
38
|
+
|
39
|
+
desc "check", "check configuration"
|
40
|
+
def check(exit_on_failure=false)
|
41
|
+
result = APIChecker.new(HaravanTheme).test_connectivity
|
42
|
+
|
43
|
+
if result.api_down?
|
44
|
+
say("Cannot connect to Haravan. API appears to be down", :red)
|
45
|
+
say("Visit http://status.haravan.com for more details", :yello)
|
46
|
+
elsif result.invalid_config?
|
47
|
+
say("Cannot connect to Haravan. Configuration is invalid.", :red)
|
48
|
+
say("Verify that your API key, password and domain are correct.", :yellow)
|
49
|
+
say("Visit https://github.com/haravan/haravan_theme#configuration for more details", :yellow)
|
50
|
+
say("If your shop domain is correct, the following URL should take you to the Private Apps page for the shop:", :yellow)
|
51
|
+
say(" https://#{config[:store]}/admin/apps/private", :yellow)
|
52
|
+
else
|
53
|
+
say("Haravan API is accessible and configuration is valid", :green) unless exit_on_failure
|
54
|
+
end
|
55
|
+
|
56
|
+
exit(1) if result.cannot_access_api? && exit_on_failure
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "configure API_KEY PASSWORD STORE THEME_ID", "generate a config file for the store to connect to"
|
60
|
+
def configure(api_key=nil, password=nil, store=nil, theme_id=nil)
|
61
|
+
config = {:api_key => api_key, :password => password, :store => store, :theme_id => theme_id, :whitelist_files => Filters::Whitelist::DEFAULT_WHITELIST}
|
62
|
+
create_file('config.yml', config.to_yaml)
|
63
|
+
check(true)
|
64
|
+
end
|
65
|
+
|
66
|
+
desc "configure_oauth ACCESS_TOKEN STORE THEME_ID", "generate a config file for the store to connect to using an OAuth access token"
|
67
|
+
def configure_oauth(access_token=nil, store=nil, theme_id=nil)
|
68
|
+
config = {:access_token => access_token, :store => store, :theme_id => theme_id}
|
69
|
+
create_file('config.yml', config.to_yaml)
|
70
|
+
end
|
71
|
+
|
72
|
+
desc "bootstrap API_KEY PASSWORD STORE THEME_NAME", "bootstrap with Timber to shop and configure local directory."
|
73
|
+
method_option :master, :type => :boolean, :default => false
|
74
|
+
method_option :version, :type => :string, :default => "latest"
|
75
|
+
def bootstrap(api_key=nil, password=nil, store=nil, theme_name=nil)
|
76
|
+
HaravanTheme.config = {:api_key => api_key, :password => password, :store => store, :whitelist_files => Filters::Whitelist::DEFAULT_WHITELIST}
|
77
|
+
check(true)
|
78
|
+
|
79
|
+
theme_name ||= 'Timber'
|
80
|
+
say("Registering #{theme_name} theme on #{store}", :green)
|
81
|
+
theme = HaravanTheme.upload_timber(theme_name, options[:version])
|
82
|
+
|
83
|
+
say("Creating directory named #{theme_name}", :green)
|
84
|
+
empty_directory(theme_name)
|
85
|
+
|
86
|
+
say("Saving configuration to #{theme_name}", :green)
|
87
|
+
HaravanTheme.config.merge!(theme_id: theme['id'])
|
88
|
+
create_file("#{theme_name}/config.yml", HaravanTheme.config.to_yaml)
|
89
|
+
|
90
|
+
say("Downloading #{theme_name} assets from Haravan")
|
91
|
+
Dir.chdir(theme_name)
|
92
|
+
download()
|
93
|
+
rescue Releases::VersionError => e
|
94
|
+
say(e.message, :red)
|
95
|
+
end
|
96
|
+
|
97
|
+
desc "download FILE", "download the shops current theme assets"
|
98
|
+
method_option :quiet, :type => :boolean, :default => false
|
99
|
+
method_option :exclude
|
100
|
+
def download(*keys)
|
101
|
+
check(true)
|
102
|
+
|
103
|
+
puts("download FILE", "download the shops current theme assets")
|
104
|
+
puts("keys: ", keys)
|
105
|
+
|
106
|
+
assets = assets_for(keys, HaravanTheme.asset_list)
|
107
|
+
|
108
|
+
if options['exclude']
|
109
|
+
assets = assets.delete_if { |asset| asset =~ Regexp.new(options['exclude']) }
|
110
|
+
end
|
111
|
+
|
112
|
+
assets.each do |asset|
|
113
|
+
download_asset(asset)
|
114
|
+
say("#{HaravanTheme.api_usage} Downloaded: #{asset}", :green) unless options['quiet']
|
115
|
+
end
|
116
|
+
say("Done.", :green) unless options['quiet']
|
117
|
+
end
|
118
|
+
|
119
|
+
desc "open", "open the store in your browser"
|
120
|
+
def open(*keys)
|
121
|
+
if Launchy.open shop_theme_url
|
122
|
+
say("Done.", :green)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
desc "upload FILE", "upload all theme assets to shop"
|
127
|
+
method_option :quiet, :type => :boolean, :default => false
|
128
|
+
def upload(*keys)
|
129
|
+
check(true)
|
130
|
+
assets = assets_for(keys, local_assets_list)
|
131
|
+
assets = keys.empty? ? local_assets_list : keys
|
132
|
+
assets.each do |asset|
|
133
|
+
send_asset(asset, options['quiet'])
|
134
|
+
end
|
135
|
+
say("Done.", :green) unless options['quiet']
|
136
|
+
end
|
137
|
+
|
138
|
+
desc "replace FILE", "completely replace shop theme assets with local theme assets"
|
139
|
+
method_option :quiet, :type => :boolean, :default => false
|
140
|
+
def replace(*keys)
|
141
|
+
check(true)
|
142
|
+
say("Are you sure you want to completely replace your shop theme assets? This is not undoable.", :yellow)
|
143
|
+
if ask("Continue? (Y/N): ") == "Y"
|
144
|
+
# only delete files on remote that are not present locally
|
145
|
+
# files present on remote and present locally get overridden anyway
|
146
|
+
remote_assets = keys.empty? ? (HaravanTheme.asset_list - local_assets_list) : keys
|
147
|
+
remote_assets.each do |asset|
|
148
|
+
delete_asset(asset, options['quiet']) unless HaravanTheme.ignore_files.any? { |regex| regex =~ asset }
|
149
|
+
end
|
150
|
+
local_assets = keys.empty? ? local_assets_list : keys
|
151
|
+
local_assets.each do |asset|
|
152
|
+
send_asset(asset, options['quiet'])
|
153
|
+
end
|
154
|
+
say("Done.", :green) unless options['quiet']
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
desc "remove FILE", "remove theme asset"
|
159
|
+
method_option :quiet, :type => :boolean, :default => false
|
160
|
+
def remove(*keys)
|
161
|
+
check(true)
|
162
|
+
keys.each do |key|
|
163
|
+
delete_asset(key, options['quiet'])
|
164
|
+
end
|
165
|
+
say("Done.", :green) unless options['quiet']
|
166
|
+
end
|
167
|
+
|
168
|
+
desc "watch", "upload and delete individual theme assets as they change, use the --keep_files flag to disable remote file deletion"
|
169
|
+
method_option :quiet, :type => :boolean, :default => false
|
170
|
+
method_option :keep_files, :type => :boolean, :default => false
|
171
|
+
def watch
|
172
|
+
check(true)
|
173
|
+
puts "Watching current folder: #{Dir.pwd}"
|
174
|
+
watcher do |filename, event|
|
175
|
+
filename = filename.gsub("#{Dir.pwd}/", '')
|
176
|
+
|
177
|
+
next unless local_assets_list.include?(filename)
|
178
|
+
|
179
|
+
action = if [:changed, :new].include?(event)
|
180
|
+
:send_asset
|
181
|
+
elsif event == :delete && !options['keep_files']
|
182
|
+
:delete_asset
|
183
|
+
else
|
184
|
+
raise NotImplementedError, "Unknown event -- #{event} -- #{filename}"
|
185
|
+
end
|
186
|
+
|
187
|
+
send(action, filename, options['quiet'])
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
desc "systeminfo", "print out system information and actively loaded libraries for aiding in submitting bug reports"
|
192
|
+
def systeminfo
|
193
|
+
ruby_version = "#{RUBY_VERSION}"
|
194
|
+
ruby_version += "-p#{RUBY_PATCHLEVEL}" if RUBY_PATCHLEVEL
|
195
|
+
puts "Ruby: v#{ruby_version}"
|
196
|
+
puts "Operating System: #{RUBY_PLATFORM}"
|
197
|
+
%w(Thor Listen HTTParty Launchy).each do |lib|
|
198
|
+
require "#{lib.downcase}/version"
|
199
|
+
puts "#{lib}: v" + Kernel.const_get("#{lib}::VERSION")
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
no_commands do
|
204
|
+
def local_assets_list
|
205
|
+
FileFilters.new(
|
206
|
+
Filters::Whitelist.new(HaravanTheme.whitelist_files),
|
207
|
+
Filters::Blacklist.new(HaravanTheme.ignore_files)
|
208
|
+
).select(local_files)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
protected
|
213
|
+
|
214
|
+
def config
|
215
|
+
@config ||= YAML.load_file 'config.yml'
|
216
|
+
end
|
217
|
+
|
218
|
+
def shop_theme_url
|
219
|
+
url = config[:store]
|
220
|
+
url += "?preview_theme_id=#{config[:theme_id]}" if config[:theme_id] && config[:theme_id].to_i > 0
|
221
|
+
url
|
222
|
+
end
|
223
|
+
|
224
|
+
private
|
225
|
+
def assets_for(keys=[], files=[])
|
226
|
+
filter = FileFilters.new(Filters::CommandInput.new(keys))
|
227
|
+
filter.select(files)
|
228
|
+
end
|
229
|
+
|
230
|
+
def watcher
|
231
|
+
FileWatcher.new(Dir.pwd).watch() do |filename, event|
|
232
|
+
yield(filename, event)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def local_files
|
237
|
+
Dir.glob(File.join('**', '*')).reject do |f|
|
238
|
+
File.directory?(f)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def download_asset(key)
|
243
|
+
return unless valid?(key)
|
244
|
+
notify_and_sleep("Approaching limit of API permits. Naptime until more permits become available!") if HaravanTheme.needs_sleep?
|
245
|
+
asset = HaravanTheme.get_asset(key)
|
246
|
+
if asset['value']
|
247
|
+
# For CRLF line endings
|
248
|
+
content = asset['value'].gsub("\r", "")
|
249
|
+
format = "w"
|
250
|
+
elsif asset['attachment']
|
251
|
+
content = Base64.decode64(asset['attachment'])
|
252
|
+
format = "w+b"
|
253
|
+
end
|
254
|
+
|
255
|
+
FileUtils.mkdir_p(File.dirname(key))
|
256
|
+
File.open(key, format) {|f| f.write content} if content
|
257
|
+
end
|
258
|
+
|
259
|
+
def send_asset(asset, quiet=false)
|
260
|
+
return unless valid?(asset)
|
261
|
+
data = {:key => asset}
|
262
|
+
content = File.read(asset)
|
263
|
+
if binary_file?(asset) || HaravanTheme.is_binary_data?(content)
|
264
|
+
content = File.open(asset, "rb") { |io| io.read }
|
265
|
+
data.merge!(:attachment => Base64.encode64(content))
|
266
|
+
else
|
267
|
+
data.merge!(:value => content)
|
268
|
+
end
|
269
|
+
|
270
|
+
response = show_during("[#{timestamp}] Uploading: #{asset}", quiet) do
|
271
|
+
HaravanTheme.send_asset(data)
|
272
|
+
end
|
273
|
+
if response.success?
|
274
|
+
say("[#{timestamp}] Uploaded: #{asset}", :green) unless quiet
|
275
|
+
else
|
276
|
+
report_error(Time.now, "Could not upload #{asset}", response)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def delete_asset(key, quiet=false)
|
281
|
+
return unless valid?(key)
|
282
|
+
response = show_during("[#{timestamp}] Removing: #{key}", quiet) do
|
283
|
+
HaravanTheme.delete_asset(key)
|
284
|
+
end
|
285
|
+
if response.success?
|
286
|
+
say("[#{timestamp}] Removed: #{key}", :green) unless quiet
|
287
|
+
else
|
288
|
+
report_error(Time.now, "Could not remove #{key}", response)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def notify_and_sleep(message)
|
293
|
+
say(message, :red)
|
294
|
+
HaravanTheme.sleep
|
295
|
+
end
|
296
|
+
|
297
|
+
def valid?(key)
|
298
|
+
return true if DEFAULT_WHITELIST.include?(key.split('/').first + "/")
|
299
|
+
say("'#{key}' is not in a valid file for theme uploads", :yellow)
|
300
|
+
say("Files need to be in one of the following subdirectories: #{DEFAULT_WHITELIST.join(' ')}", :yellow)
|
301
|
+
false
|
302
|
+
end
|
303
|
+
|
304
|
+
def binary_file?(path)
|
305
|
+
mime = MimeMagic.by_path(path)
|
306
|
+
say("'#{path}' is an unknown file-type, uploading asset as binary", :yellow) if mime.nil? && ENV['TEST'] != 'true'
|
307
|
+
mime.nil? || !mime.text?
|
308
|
+
end
|
309
|
+
|
310
|
+
def report_error(time, message, response)
|
311
|
+
say("[#{timestamp(time)}] Error: #{message}", :red)
|
312
|
+
say("Error Details: #{errors_from_response(response)}", :yellow)
|
313
|
+
end
|
314
|
+
|
315
|
+
def errors_from_response(response)
|
316
|
+
object = {status: response.headers['status'], request_id: response.headers['x-request-id']}
|
317
|
+
|
318
|
+
errors = response.parsed_response ? response.parsed_response["errors"] : response.body
|
319
|
+
|
320
|
+
object[:errors] = case errors
|
321
|
+
when NilClass
|
322
|
+
''
|
323
|
+
when String
|
324
|
+
errors.strip
|
325
|
+
else
|
326
|
+
errors.values.join(", ")
|
327
|
+
end
|
328
|
+
object.delete(:errors) if object[:errors].length <= 0
|
329
|
+
object
|
330
|
+
end
|
331
|
+
|
332
|
+
def show_during(message = '', quiet = false, &block)
|
333
|
+
print(message) unless quiet
|
334
|
+
result = yield
|
335
|
+
print("\r#{' ' * message.length}\r") unless quiet
|
336
|
+
result
|
337
|
+
end
|
338
|
+
|
339
|
+
def timestamp(time = Time.now)
|
340
|
+
time.strftime(TIMEFORMAT)
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
HaravanTheme.configureMimeMagic
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'haravan_theme/filters/blacklist'
|
2
|
+
require 'haravan_theme/filters/whitelist'
|
3
|
+
require 'haravan_theme/filters/command_input'
|
4
|
+
|
5
|
+
module HaravanTheme
|
6
|
+
class FileFilters
|
7
|
+
def initialize(*filters)
|
8
|
+
raise ArgumentError, "Must have at least one filter to apply" unless filters.length > 0
|
9
|
+
@filters = filters
|
10
|
+
end
|
11
|
+
|
12
|
+
def select(list)
|
13
|
+
@filters.reduce(list) do |results, filter|
|
14
|
+
filter.select(results)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module HaravanTheme
|
2
|
+
module Filters
|
3
|
+
class Blacklist
|
4
|
+
attr_reader :patterns
|
5
|
+
|
6
|
+
def initialize(pattern_strings=[])
|
7
|
+
@patterns = pattern_strings.map { |p| Regexp.new(p)}
|
8
|
+
end
|
9
|
+
|
10
|
+
def select(list)
|
11
|
+
list.select do |entry|
|
12
|
+
patterns.none? { |pat| pat.match(entry) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module HaravanTheme
|
2
|
+
module Filters
|
3
|
+
class CommandInput
|
4
|
+
attr_reader :patterns
|
5
|
+
def initialize(inputs=[])
|
6
|
+
@patterns = inputs.map { |i| Regexp.compile(i) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def select(list)
|
10
|
+
return list if patterns.empty?
|
11
|
+
list.select { |entry|
|
12
|
+
patterns.any? { |pat| pat.match(entry) }
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module HaravanTheme
|
2
|
+
module Filters
|
3
|
+
class Whitelist
|
4
|
+
DEFAULT_WHITELIST = %w(layout/ assets/ config/ snippets/ templates/ locales/)
|
5
|
+
|
6
|
+
attr_reader :patterns
|
7
|
+
|
8
|
+
def initialize(pattern_strings=[])
|
9
|
+
@patterns = (pattern_strings.empty? ? DEFAULT_WHITELIST : pattern_strings).map { |pattern| Regexp.new(pattern) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def select(list)
|
13
|
+
list.select do |entry|
|
14
|
+
patterns.any? { |pat| pat.match(entry) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'rss'
|
3
|
+
|
4
|
+
module HaravanTheme
|
5
|
+
FEED_URL = 'https://github.com/Haravan/Timber/releases.atom'
|
6
|
+
ZIP_URL = 'https://github.com/Haravan/Timber/archive/%s.zip'
|
7
|
+
|
8
|
+
class Releases
|
9
|
+
class VersionError < StandardError; end
|
10
|
+
Release = Struct.new(:version) do
|
11
|
+
def zip_url
|
12
|
+
ZIP_URL % version
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def fetch!
|
17
|
+
response = HTTParty.get(FEED_URL)
|
18
|
+
raise "Could not retrieve feed from #{FEED_URL}" if response.code != 200
|
19
|
+
@feed = RSS::Parser.parse(response.body)
|
20
|
+
end
|
21
|
+
|
22
|
+
def all
|
23
|
+
@all ||= begin
|
24
|
+
versioned_releases.reduce({'master' => master, 'latest' => latest}) do |all, release|
|
25
|
+
all[release.version] = release
|
26
|
+
all
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def find(version)
|
32
|
+
release = all[version]
|
33
|
+
if release.nil?
|
34
|
+
error = [
|
35
|
+
"Invalid version '#{version}'.",
|
36
|
+
"Valid versions are:",
|
37
|
+
].concat(all.keys.map{|v| " #{v}"})
|
38
|
+
raise VersionError, error.join("\n")
|
39
|
+
end
|
40
|
+
release
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def versioned_releases
|
45
|
+
fetch! unless @feed
|
46
|
+
@versioned_releases ||= @feed.items.map { |item| Release.new(item.title.content) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def latest
|
50
|
+
Release.new(versioned_releases.first.version)
|
51
|
+
end
|
52
|
+
|
53
|
+
def master
|
54
|
+
Release.new('master')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'base64'
|
3
|
+
module HaravanTheme
|
4
|
+
REMOTE_CERT_FILE = 'http://curl.haxx.se/ca/cacert.pem'
|
5
|
+
CA_CERT_FILE = File.expand_path(File.dirname(__FILE__) + '/certs/cacert.pem')
|
6
|
+
include HTTParty
|
7
|
+
ssl_ca_file CA_CERT_FILE
|
8
|
+
@@current_api_call_count = 0
|
9
|
+
@@total_api_calls = 40
|
10
|
+
|
11
|
+
NOOPParser = Proc.new {|data, format| {} }
|
12
|
+
TIMER_RESET = 10
|
13
|
+
PERMIT_LOWER_LIMIT = 3
|
14
|
+
MAX_TIMBER_RETRY = 5
|
15
|
+
|
16
|
+
|
17
|
+
def self.test?
|
18
|
+
ENV['test']
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.manage_timer(response)
|
22
|
+
return unless response.headers['x-haravan-shop-api-call-limit']
|
23
|
+
@@current_api_call_count, @@total_api_calls = response.headers['x-haravan-shop-api-call-limit'].split('/')
|
24
|
+
@@current_timer = Time.now if @current_timer.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.critical_permits?
|
28
|
+
@@total_api_calls.to_i - @@current_api_call_count.to_i < PERMIT_LOWER_LIMIT
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.passed_api_refresh?
|
32
|
+
delta_seconds > TIMER_RESET
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.delta_seconds
|
36
|
+
Time.now.to_i - @@current_timer.to_i
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.needs_sleep?
|
40
|
+
critical_permits? && !passed_api_refresh?
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.sleep
|
44
|
+
if needs_sleep?
|
45
|
+
Kernel.sleep(TIMER_RESET - delta_seconds)
|
46
|
+
@current_timer = nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.api_usage
|
51
|
+
"[API Limit: #{@@current_api_call_count || "??"}/#{@@total_api_calls || "??"}]"
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
def self.asset_list
|
56
|
+
# HTTParty parser chokes on assest listing, have it noop
|
57
|
+
# and then use a rel JSON parser.
|
58
|
+
response = haravan.get(path, :parser => NOOPParser)
|
59
|
+
manage_timer(response)
|
60
|
+
|
61
|
+
assets = JSON.parse(response.body)["assets"].collect {|a| a['key'] }
|
62
|
+
# Remove any .css files if a .css.liquid file exists
|
63
|
+
assets.reject{|a| assets.include?("#{a}.liquid") }
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.get_asset(asset)
|
67
|
+
response = haravan.get(path, :query =>{:asset => {:key => asset}}, :parser => NOOPParser)
|
68
|
+
manage_timer(response)
|
69
|
+
|
70
|
+
# HTTParty json parsing is broken?
|
71
|
+
asset = response.code == 200 ? JSON.parse(response.body)["asset"] : {}
|
72
|
+
asset['response'] = response
|
73
|
+
asset
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.send_asset(data)
|
77
|
+
response = haravan.put(path, :body => JSON.generate({:asset => data} ))
|
78
|
+
manage_timer(response)
|
79
|
+
response
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.delete_asset(asset)
|
83
|
+
response = haravan.delete(path, :body =>{:asset => {:key => asset}})
|
84
|
+
manage_timer(response)
|
85
|
+
response
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.upload_timber(name, version, retries=0)
|
89
|
+
release = Releases.new.find(version)
|
90
|
+
response = haravan.post("/admin/themes.json", :body => {:theme => {:name => name, :src => release.zip_url, :role => 'unpublished'}})
|
91
|
+
manage_timer(response)
|
92
|
+
body = JSON.parse(response.body)
|
93
|
+
if theme = body['theme']
|
94
|
+
puts "Successfully created #{name} using Haravan Timber #{version}"
|
95
|
+
watch_until_processing_complete(theme)
|
96
|
+
elsif retries < MAX_TIMBER_RETRY
|
97
|
+
upload_timber(name, version, retries + 1)
|
98
|
+
else
|
99
|
+
puts "Could not download theme!"
|
100
|
+
puts body
|
101
|
+
exit 1
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.config
|
106
|
+
@config ||= if File.exist? 'config.yml'
|
107
|
+
config = YAML.load(File.read('config.yml'))
|
108
|
+
config
|
109
|
+
else
|
110
|
+
puts "config.yml does not exist!" unless test?
|
111
|
+
{}
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.config=(config)
|
116
|
+
@config = config
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.path
|
120
|
+
@path ||= config[:theme_id] ? "/admin/themes/#{config[:theme_id]}/assets.json" : "/admin/assets.json"
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.ignore_files
|
124
|
+
(config[:ignore_files] || []).compact
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.whitelist_files
|
128
|
+
(config[:whitelist_files] || []).compact
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.is_binary_data?(string)
|
132
|
+
if string.respond_to?(:encoding)
|
133
|
+
string.encoding == "US-ASCII"
|
134
|
+
else
|
135
|
+
( string.count( "^ -~", "^\r\n" ).fdiv(string.size) > 0.3 || string.index( "\x00" ) ) unless string.empty?
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.check_config
|
140
|
+
haravan.get(path).code == 200
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.get_index
|
144
|
+
puts(config[:theme_id] ? '' : "Hãy nhập theme_id")
|
145
|
+
haravan.get(path)
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
def self.haravan
|
150
|
+
basicAuth = Base64.strict_encode64(config[:api_key] + ':' + config[:password])
|
151
|
+
headers 'accept' => '*/*'
|
152
|
+
headers 'Content-Type' => 'application/json'
|
153
|
+
headers 'Authorization' => 'Basic ' + basicAuth
|
154
|
+
base_uri "https://#{config[:store]}"
|
155
|
+
|
156
|
+
puts(base_uri)
|
157
|
+
|
158
|
+
HaravanTheme
|
159
|
+
end
|
160
|
+
|
161
|
+
def self.watch_until_processing_complete(theme)
|
162
|
+
count = 0
|
163
|
+
while true do
|
164
|
+
Kernel.sleep(count)
|
165
|
+
response = haravan.get("/admin/themes/#{theme['id']}.json")
|
166
|
+
theme = JSON.parse(response.body)['theme']
|
167
|
+
return theme if theme['previewable']
|
168
|
+
count += 5
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
data/shipit.rubygems.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# using the default shipit config
|