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.
@@ -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,3 @@
1
+ module HaravanTheme
2
+ VERSION = "0.0.25"
3
+ 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
@@ -0,0 +1 @@
1
+ # using the default shipit config