lib/locomotive/mounter/writer/api/theme_assets_writer.rb
module Locomotive
module Mounter
module Writer
module Api
# Push theme assets to a remote LocomotiveCMS engine.
#
# New assets are automatically pushed.
# Existing ones are not pushed unless the :force option is
# passed OR if the size of the asset (if not a javascript or stylesheet) has changed.
#
class ThemeAssetsWriter < Base
# Other local attributes
attr_accessor :tmp_folder
# store checksums of remote assets. needed to check if an asset has to be updated or not
attr_accessor :checksums
# the assets stored in the engine have the same base url
attr_accessor :remote_base_url
# cache the compiled theme assets to avoid to perform compilation more than once
attr_accessor :cached_compiled_assets
def prepare
super
self.checksums = {}
self.cached_compiled_assets = {}
# prepare the place where the assets will be stored temporarily.
self.create_tmp_folder
# assign an _id to a local content type if possible
self.get(:theme_assets, nil, true).each do |attributes|
remote_path = File.join(attributes['folder'], File.basename(attributes['local_path']))
if theme_asset = self.theme_assets[remote_path]
theme_asset._id = attributes['id']
self.checksums[theme_asset._id] = attributes['checksum']
end
if remote_base_url.nil?
attributes['url'] =~ /(.*\/sites\/[0-9a-f]+\/theme)/
self.remote_base_url = $1
end
end
end
def write
self.theme_assets_by_priority.each do |theme_asset|
# track it in the logs
self.output_resource_op theme_asset
status = :skipped
errors = []
file = self.build_temp_file(theme_asset)
params = theme_asset.to_params.merge(source: file, performing_plain_text: false)
begin
if theme_asset.persisted?
# we only update it if the size has changed or if the force option has been set.
if self.force? || self.theme_asset_changed?(theme_asset)
response = self.put :theme_assets, theme_asset._id, params
status = self.response_to_status(response)
else
status = :same
end
else
response = self.post :theme_assets, params, nil, true
status = self.response_to_status(response)
end
rescue Exception => e
if self.force?
status, errors = :error, e.message
else
raise e
end
end
# very important. we do not want a huge number of non-closed file descriptor.
file.close
# track the status
self.output_resource_op_status theme_asset, status, errors
end
# make the stuff like they were before
self.remove_tmp_folder
end
protected
# Create the folder to store temporarily the files.
#
def create_tmp_folder
self.tmp_folder = self.runner.parameters[:tmp_dir] || File.join(Dir.getwd, '.push-tmp')
FileUtils.mkdir_p(self.tmp_folder)
end
# Clean the folder which had stored temporarily the files.
#
def remove_tmp_folder
FileUtils.rm_rf(self.tmp_folder) if self.tmp_folder
end
# Build a temp file from a theme asset.
#
# @param [ Object ] theme_asset The theme asset
#
# @return [ File ] The file descriptor
#
def build_temp_file(theme_asset)
path = File.join(self.tmp_folder, theme_asset.path)
FileUtils.mkdir_p(File.dirname(path))
File.open(path, 'w') do |file|
file.write(self.content_of(theme_asset))
end
File.new(path)
end
# Shortcut to get all the theme assets.
#
# @return [ Hash ] The hash whose key is the slug and the value is the snippet itself
#
def theme_assets
return @theme_assets if @theme_assets
@theme_assets = {}.tap do |hash|
self.mounting_point.theme_assets.each do |theme_asset|
hash[theme_asset.path] = theme_asset
end
end
end
# List of theme assets sorted by their priority.
#
# @return [ Array ] Sorted list of the theme assets
#
def theme_assets_by_priority
self.theme_assets.values.sort { |a, b| a.priority <=> b.priority }
end
# Tell if the theme_asset has been changed in order to update it
# if so or simply skip it.
#
# @param [ Object ] theme_asset The theme asset
#
# @return [ Boolean ] True if the checksums of the local and remote files are different.
#
def theme_asset_changed?(theme_asset)
content = self.content_of(theme_asset)
if theme_asset.stylesheet_or_javascript?
# we need to compare compiled contents (sass, coffeescript) with the right urls inside
content = content.gsub(/[("'](\/(stylesheets|javascripts|fonts|images|media|others)\/(([^;.]+)\/)*([a-zA-Z_\-0-9]+)\.[a-z]{2,3})[)"']/) do |path|
sanitized_path = path.gsub(/[("')]/, '').gsub(/^\//, '')
sanitized_path = File.join(self.remote_base_url, sanitized_path)
"#{path.first}#{sanitized_path}#{path.last}"
end
end
# compare local checksum with the remote one
Digest::MD5.hexdigest(content) != self.checksums[theme_asset._id]
end
# Return the content of a theme asset.
# If the theme asset is either a stylesheet or javascript file,
# it uses Sprockets to compile it.
# Otherwise, it returns the raw content of the asset.
#
# @return [ String ] The content of the theme asset
#
def content_of(theme_asset)
if theme_asset.stylesheet_or_javascript?
if self.cached_compiled_assets[theme_asset.path].nil?
self.cached_compiled_assets[theme_asset.path] = self.sprockets[theme_asset.short_path].to_s
end
self.cached_compiled_assets[theme_asset.path]
else
theme_asset.content
end
end
def sprockets
Locomotive::Mounter::Extensions::Sprockets.environment(self.mounting_point.path, true)
end
end
end
end
end
end