Skip to content

Instantly share code, notes, and snippets.

@tanelj
Last active March 18, 2019 14:51
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tanelj/a64d58185551976874d5 to your computer and use it in GitHub Desktop.
Save tanelj/a64d58185551976874d5 to your computer and use it in GitHub Desktop.
Small script to migrate content between websites that are build with Voog CMS (www.voog.com). Read more about Voog API www.voog.com/developers/api
#!/usr/bin/env ruby
# voog_migrator.rb
#
# Small script to migrate content between websites that are build with Voog CMS.
# This tool is using Ruby client of the Voog API: https://github.com/Voog/voog.rb.
#
# Read more about Voog API http://www.voog.com/developers/api.
# This tools allows to analyze source and target host (default action when running this script without any parameters)
# and migrate requested sub-pages or all pages from source host.
# Limitations:
# * Layouts used by migratable pages (whit exactly same name) should be exits in target host (you can copy them manually)
# * Files and photos are not migrated
# * When migration gallery object then asset elements in it is copied only when source host same as target host. Otherwise only empty gallery
# is created (files should be uploaded and added to galleries manually)
# * Blog comments are not migrated
# * Form tickets are not migrated
# * Site users are not migrated
# * Menu links (page.content_type is "link") are not migrated
# Prerequisites:
# * API tokens for your Voog sites. Read more how to generate API tokens: https://github.com/Voog/voog-kit/#api-token.
# Step 1: Set source and target host (and optional `paths` attribute if needed) parameters in `@conf` variable.
# Step 2: Run this script without any parameter to get statistics information about used languages, layout names and paths to migrate:
# ./voog_migrator.rb
# Step 3: Copy all layouts that are listed in layouts "warning" section.
# Step 4: Running script with parameter "--migrate=true" to run migration:
# ./voog_migrator.rb --migrate=true
# Step 5 (optional): Download all files present in "--> Used assets" section (script output) and upload them to your Voog site. Example (using wget program):
# mkdir -p site-files
# cd site-files
# wget http://example.com/photos/me.jpg
# wget ....
require 'voog_api'
require 'pp'
# This wrapper class is used to communicate with Voog API.
# All main requests are cached.
#
# Usage:
# @source = VoogSite.new('source-host.voog.com', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', protocol: 'http')
# @source.build_site_tree! # Fetches all required data and build site tree object. NB! it manipulates fetched nodes objects directly.
#
class VoogSite
attr_accessor :host, :token, :protocol, :per_page, :debug, :auto_paginate
def initialize(host, token, opt = {})
@host = host
@token = token
@protocol = opt.fetch(:protocol, 'http')
@per_page = opt.fetch(:per_page, 250)
@auto_paginate = opt.fetch(:auto_paginate, true)
@debug = opt.fetch(:debug, false)
end
# Initialize Voog API client
def client
@client ||= Voog::Client.new(@host, @token, raise_on_error: true, protocol: @protocol)
end
# Get all layouts (components are included)
def all_layouts(opt = {})
@all_layouts ||= paginate(:layouts, opt)
end
# Get layouts only (without components)
def layouts(opt = {})
@layouts ||= all_layouts(opt).select { |e| e.content_type != 'component' }
end
# Get all languages
def languages(opt = {})
@languages ||= paginate(:languages, opt)
end
# Get all nodes
def nodes(opt = {})
@nodes ||= paginate(:nodes, opt)
end
# Get all pages
def pages(opt = {})
@pages ||= paginate(:pages, opt.merge('q.page.content_type.$not_eq' => 'link'))
end
# Get all pages
def element_definitions(opt = {})
@element_definitions ||= paginate(:element_definitions, opt)
end
# Get all people
def people(opt = {})
@people ||= paginate(:people, opt)
end
# Return language codes user by site
def language_codes
@language_codes ||= languages.map(&:code)
end
# Return paths user by site
def page_paths
@page_paths ||= pages.map(&:path)
end
# Build site tree.
# It adds child nodes to each node and all pages to related node object.
# NB! It manipulates node objects directly. E.g after site tree is built the its possible to use this code:
# node = node.first
# node.children => [....]
# node.pages => {en: obj, et: obj}
#
# Options:
# reload - if true then clears cache before builds the tree (default: false)
def build_site_tree!(opt = {})
clear_cache! if opt.fetch(:reload, false)
site_tree
end
# Detect design kind
def has_custom_design?
layouts.first.mime_type == 'application/vnd.voog.design.custom+liquid'
end
# Add newly create language to language objects. Clears cache and rebuilds site tree.
def push_language!(language)
clear_cache!
site_tree
end
# Add newly created page to pages set and rebuild site tree.
def push_page!(page)
return unless @pages
@pages << page if @pages
@nodes << client.node(page.node.id) unless node_by_id(page.node.id)
@site_tree = nil
@nodes_tree = nil
@node_id_hash = nil
@pages_id_hash = nil
@pages_path_hash = nil
# rebuild site tree
site_tree
end
# Add newly created element definition to element definition set.
def push_element_definition!(element_definition)
return unless @element_definitions
@element_definitions << element_definition
@element_definition_title_hash = nil
end
# Build site tree. Adds pages to nodes.
def site_tree
@site_tree ||= begin
root = nodes_tree
build_node_pages(root)
root
end
end
# Build nodes tree. Add nodes objects to parent node as .children.
def nodes_tree
@nodes_tree ||= begin
root = nodes.detect { |e| e.parent_id.nil? } || {}
build_nodes_tree(root, nodes.clone) unless root.empty?
root
end
end
# Prints out site tree
# Options:
# lang - language code. If present then page names for requested language are shown. Otherwise it shows nodes default title.
def print_site_tree(opt = {})
if opt.key?(:lang)
puts "Site language tree (#{opt[:lang]}):\n"
else
puts "Site nodes tree:\n"
end
print_tree(site_tree, 0, opt)
end
# Prints out requested tree object.
def print_tree(obj, nesting = 0, opt = {})
from_level = opt.fetch(:from_level, 0)
lang = opt.fetch(:lang, nil)
title = if lang
obj.pages && obj.pages[lang.to_sym] ? "#{obj.pages[lang.to_sym].title} (#{obj.pages[lang.to_sym].path})" : '--missing--'
else
"#{obj.title} (#{obj.pages.keys.join(', ') if obj.pages})"
end
puts "| " * nesting + "|-- #{title}" if nesting >= from_level
obj[:children].each do |c|
print_tree(c, nesting + 1, opt)
end
end
# Fetch all elements for requested API resource
def paginate(api_method, opt = {})
opt[:per_page] = 250 if @auto_paginate
data = client.send(api_method, opt)
last_response = client.last_response
if @auto_paginate
# Where there has "next" key in response Links header then load next page.
while last_response.rels[:next]
puts last_response.rels[:next].href if @debug
last_response = last_response.rels[:next].get
data.concat(last_response.data) if last_response.data.is_a?(Array)
end
end
data
end
# Get node by id
def node_by_id(id)
@node_id_hash ||= nodes.each_with_object({}) { |e, h| h[e.id] = e }
@node_id_hash[id]
end
# Get page by id
def page_by_id(id)
@pages_id_hash ||= pages.each_with_object({}) { |e, h| h[e.id] = e }
@pages_id_hash[id]
end
# Get page by path
def page_by_path(path)
@pages_path_hash ||= pages.each_with_object({}) { |e, h| h[e.path] = e }
@pages_path_hash[path]
end
# Get page by path
def element_definition_by_title(title)
@element_definition_title_hash ||= element_definitions.each_with_object({}) { |e, h| h[e.title] = e }
@element_definition_title_hash[title]
end
# Clear site cache
def clear_cache!
@all_layouts = nil
@layouts = nil
@languages = nil
@nodes = nil
@pages = nil
@element_definitions = nil
@site_tree = nil
@nodes_tree = nil
@node_id_hash = nil
@pages_id_hash = nil
@pages_path_hash = nil
@element_definition_title_hash = nil
@people = nil
end
private
def build_node_pages(node)
return if node.nil? || node.empty?
node.pages = {}
pages.each { |e| node.pages[e.language.code.to_sym] = e if e.node.id == node.id }
node[:children].each do |c|
build_node_pages(c)
end
end
def build_nodes_tree(parent, arr)
parent.children = select_node_children(arr, parent.id)
parent.children.each do |c|
build_nodes_tree(c, arr)
end
end
def select_node_children(arr, parent_id)
children, arr = arr.partition { |e| e.parent_id == parent_id }
children.map { |e| e }
end
end
# Set of method that allows to analyze source and target hosts and move data between hosts.
#
# Usage:
# @conf = {
# # paths: ['products'],
# source_host: 'source-host.voog.com', source_api_token: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', source_protocol: 'http',
# target_host: 'target-host.voog.com', target_api_token: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', target_protocol: 'http'
# }
#
# @migrator = VoogSiteMigrator.new(@conf)
# @migrator.run_migration_analyze
# @migrator.migrate_all!
#
class VoogSiteMigrator
attr_accessor :source, :target, :paths, :debug
# Required minimal set of parameters:
# source_host - source host name
# source_api_token - source host name
# target_host - source host name
# target_api_token - target api
#
# Optional parameters:
# paths - array of paths. If not give then whole site is migrated otherwise only paths in array are migrate (including sub-pages and parent parent pages (if missing in target)).
# migrate_element_definitions - default "true". If true then all element definition is migrated to target site.
# debug - boolean. Default is "false".
def initialize(opt = {})
@debug = opt.fetch(:debug, false)
@paths = opt.fetch(:paths, [])
@source = VoogSite.new(opt[:source_host], opt[:source_api_token], protocol: opt[:source_protocol], auto_paginate: true, debug: @debug)
@target = VoogSite.new(opt[:target_host], opt[:target_api_token], protocol: opt[:target_protocol], auto_paginate: true, debug: @debug)
@migrate_element_definitions = opt.fetch(:migrate_element_definitions, true)
# TODO: implement:
# @languages = opt.fetch(:languages, nil) # Limit languages
@rewrite_prefixes = opt.fetch(:rewrite_prefixes, {}) # Url prefixes rewrite
# @ignore_prefixes = opt.fetch(:ignore_prefixes, {}) # Url prefixes to ignore
end
# Run migration analyzer to get statistics about source and target hosts and about required layouts.
def run_migration_analyze
puts "\n--------------- Migration analyze ---------------\n\n"
puts "-> Source host: #{source.host}"
puts "-> Target host: #{target.host}\n\n"
if [source.host, source.token, target.host, target.token].any?(&:nil?)
puts '-> Source host: is missing!' if source.host.nil?
puts '-> Source token: is missing!' if source.token.nil?
puts '-> Target host: is missing!' if target.host.nil?
puts '-> Target token: is missing!' if target.token.nil?
fail
end
puts "-> Loading sites data ..."
ensure_cache!
puts "\n"
lang_diff = language_diff
puts "\n---- Language stats ----\n"
puts "-> Languages present in both hosts: #{lang_diff[:common].join(', ')}\n" unless lang_diff[:common].empty?
puts "-> Languages present only source host: #{lang_diff[:source_only].join(', ')}\n" unless lang_diff[:source_only].empty?
puts "-> Languages present only target host: #{lang_diff[:target_only].join(', ')}\n" unless lang_diff[:target_only].empty?
if @migrate_element_definitions
definition_diff = array_diff(source.element_definitions.map(&:title), target.element_definitions.map(&:title))
puts "\n---- Element definitions stats ----\n"
puts "-> Element definitions present in both hosts: #{definition_diff[:common].join(', ')}\n" unless definition_diff[:common].empty?
puts "-> Element definitions present only source host: #{definition_diff[:source_only].join(', ')}\n" unless definition_diff[:source_only].empty?
puts "-> Element definitions present only target host: #{definition_diff[:target_only].join(', ')}\n" unless definition_diff[:target_only].empty?
end
puts "\n---- Page migration data ----\n"
unless @paths.empty?
puts "-> Requested paths (#{@paths.size}): \n"
puts @paths.join("\n")
not_found_in_source = (@paths - source.page_paths).sort
puts "\n-> ERROR: Migratable pages that are not found in source host (#{not_found_in_source.size}):\n#{not_found_in_source.join("\n")}\n" unless not_found_in_source.empty?
else
puts "-> All pages requested.\n"
end
found_in_target = (migratable_paths & target.page_paths.map { |e| get_target_path(e) } & source.page_paths).sort
puts "\n-> WARNING: Migratable pages that are already present in target host (#{found_in_target.size}):\n#{found_in_target.join("\n")}\n" unless found_in_target.empty?
puts "\n-> Pages to be migrated (#{migratable_paths.size}):\n"
puts migratable_paths.map { |p| "#{p} -> #{get_target_path(p)}" }.join("\n")
puts "\n--- Layouts ---"
puts "-> Source host is using: #{source.has_custom_design? ? 'custom design' : 'stock design'}"
puts "-> Target host is using: #{target.has_custom_design? ? 'custom design' : 'stock design'}\n\n"
target_layouts = target.layouts.map(&:title).sort
puts "-> Layouts present in target host:\n"
puts target_layouts.join("\n")
puts "\n-> Layouts used by migratable pages.\n"
puts required_layouts.join("\n")
missing_layouts = required_layouts - target.layouts.map(&:title)
if missing_layouts.empty?
puts "\n-> Ok all used layouts are present in target host.\n"
else
puts "\n-> ERROR: Some layout are missing form target host (#{missing_layouts.size}):\n"
puts missing_layouts.join("\n")
end
puts "\n\n"
end
# Migrates all migratable pages from source to target site.
def migrate_all!
puts "\n--------------- Migrating site data #{Time.now.strftime('%Y-%m-%d %H:%M')}---------------\n\n"
puts "-> Source host: #{source.host}"
puts "-> Target host: #{target.host}\n\n"
puts "-> Loading sites data ..."
ensure_cache!
if @migrate_element_definitions
puts "\n-> Element definitions migration is enabled.\n"
migrate_element_definitions!
puts "\n"
end
puts "\n-> Migratable paths (#{migratable_paths.size}):\n"
puts migratable_paths.join("\n")
puts "\n-> Starting migration ...\n"
migratable_paths.each do |path|
migrate_path!(path)
end
puts "\n Done at #{Time.now.strftime('%Y-%m-%d %H:%M')}."
end
# Migrate all element definitions
def migrate_element_definitions!
puts "\n-> Start to fetch and migrate element definitions #{migrateble_definitions.join(', ')}:\n"
source.element_definitions.select { |e| migrateble_definitions.include?(e.title) }.each do |ed|
puts "--> Migrating element definition \"#{ed.title}\" ..."
definition = ed.rels[:self].get.data
fields = if definition.data && definition.data.properties
definition.data.properties.to_attrs.map do |k, v|
v[:key] ||= k.to_s
v
end
else
[]
end
new_definition = target.client.create_element_definition(title: definition.title, fields: fields)
target.push_element_definition!(new_definition)
end
end
# Migrate requested path.
# It finds page object by path and runs migration for it.
# Options:
# lang - when present then the path should be under given language.
def migrate_path!(path, opt = {})
ensure_cache!
lang = if path.size == 2
# Allow migrate languages
l = path
path = ''
l
else
opt.fetch(:lang, nil)
end
page = lang ? source.pages.detect { |p| p.path.to_s == path && p.language.code == lang.to_s } : source.page_by_path(path)
if page
migrate_page!(page)
else
# TODO: Handle new parent page creation when it's missing in source
puts "-> ERROR: Page with path '#{path}' was not found in source host!"
end
end
# Migrate page.
# Creates also parent page when it's missing in target site.
# Parameters:
# page - page object
def migrate_page!(page)
puts "\n-> Migrating page '#{page.path}' (language: #{page.language.code})"
target_path = get_target_path(page.path)
puts "--> NOTICE: Using rewritten target path '#{target_path}'." if page.path != target_path
if !page.path.to_s.empty? && target.page_by_path(target_path)
puts "--> SKIPPING: Page with path '#{target_path}' is already in target host."
elsif page.path.to_s.empty? && target.language_codes.include?(page.language.code)
puts "--> SKIPPING: Root page for language #{page.language.code} is already in target host."
elsif page.path.to_s.empty? && !target.language_codes.include?(page.language.code)
# Handle language root page migration
migrate_language!(page)
else
# Migrate normal page
# Try to find parent page
path_slugs = target_path.split('/')
new_slug = path_slugs.pop
parent_path = path_slugs.join('/')
# Try to find existing one
parent_page = target.pages.detect { |p| p.path.to_s == parent_path && p.language.code == page.language.code }
# Seems missing, try to create new parent
parent_page = migrate_path!(parent_path, lang: page.language.code) unless parent_page
if parent_page
# Detect existing pages under same node
translated_paths = source.node_by_id(page.node.id).pages.map { |lang_code, p| p.path if lang_code.to_s != page.language.code}.compact
# Check if there has some translated path in target host
related_path = (translated_paths & target.page_paths).detect { |p| target.page_by_path(p).language.code != page.language.code }
if related_path
node = target.page_by_path(related_path).node
# Add to existing node when there isn't all ready some other page in current language
related_node = node if target.node_by_id(node.id).pages[page.language.code.to_sym].nil?
end
create_page_from_source!(page, parent: parent_page, node: related_node, new_slug: new_slug)
else
puts "--> ERROR: Parent page '#{parent_page}' not found in target host! Skipping all children."
end
end
end
# Migrate language.
# NB! The root page is created automatically when new language is added to site.
#
# Read more: http://www.voog.com/developers/api/resources/languages#create_language
def migrate_language!(source_page)
puts "--> Create language #{source_page.language.code} ..."
source_language = source.languages.detect { |e| e.code == source_page.language.code }
allowed_attr = %i(code title site_title site_header)
data = source_language.to_attrs.select { |k, _| allowed_attr.include?(k) }
# Create missing language
language = target.client.create_language(data)
if language
# Migrate language related contents
migrate_content!(source_language, language, parent_kind: Voog::API::Contents::ParentKind::Language)
# Push language and trigger site tree rebuild
target.push_language!(language)
# Find and update language related root page
new_page = target.pages.detect { |e| e.path.to_s == '' && e.language.code == language.code }
if new_page
puts "---> Updating root page of the language #{language.code}"
layout = target.layouts.detect { |e| e.title == source_page.layout.title }
allowed_attr = %i(title keywords description slug data hidden publishing isprivate)
data = source_page.to_attrs.select { |k, _| allowed_attr.include?(k) }.tap do |h|
h[:layout_id] = layout.id if layout
end.reject { |k, v| v.nil? }
target.client.update_page(new_page.id, data)
migrate_content!(source_page, new_page)
case new_page.content_type
when 'blog'
migrate_articles!(source_page, new_page)
when 'elements'
migrate_elements!(source_page, new_page)
end
puts '---> Done'
new_page
else
puts "--> ERROR: Language creation was failed. Page for language #{language.code} is missing in target host!"
end
else
puts "--> ERROR: Language creation was failed. Code #{source_page.language.code}!"
end
end
# Migrate page from source to target host.
#
# Read more: http://www.voog.com/developers/api/resources/pages#create_pages
def create_page_from_source!(page, opt = {})
puts "--> Create page from '#{page.path}' ..."
layout = target.layouts.detect { |e| e.title == page.layout.title }
# TODO: Implement fallback layout by configuration
if layout
allowed_attr = %i(title path keywords description slug data hidden publishing)
data = page.to_attrs.select { |k, _| allowed_attr.include?(k) }.tap do |h|
h[:layout_id] = layout.id
language = target.languages.detect { |e| e.code == page.language.code }
h[:language_id] = language.id if language
puts "----> Debug: language missing. #{page.inspect}" unless language
h[:parent_id] = opt[:parent].id if opt[:parent]
h[:node_id] = opt[:node].id if opt[:node] if page.slug == opt[:new_slug]
h[:slug] = opt[:new_slug] unless opt[:new_slug].to_s.empty?
end
new_page = target.client.create_page(data)
if new_page.slug != page.slug
if new_page.slug == opt[:new_slug]
puts "---> NOTICE: New page was created with rewritten path '#{new_page.slug}' (was: '#{page.slug}')"
else
puts "---> WARNING: New page was created with different path '#{new_page.slug}' (excepted: '#{page.slug}')"
end
end
if new_page.id
# Set page privacy
target.client.update_page(new_page.id, privacy: page.isprivate)
# Migrate content
migrate_content!(page, new_page)
case new_page.content_type
when 'blog'
migrate_articles!(page, new_page)
when 'elements'
migrate_elements!(page, new_page)
end
end
puts "--> Done."
target.push_page!(new_page)
new_page
else
puts "--> ERROR: Layout with name '#{page.layout.title}' was not found in target host! Skipping page '#{page.path}'."
end
end
# Migrate content from source to target host.
#
# Options:
# parent_kind - default is Voog::API::Contents::ParentKind::Page. Allowed values: ::Page, ::Language, ::Article and ::Element.
#
# Read more: http://www.voog.com/developers/api/resources/contents#create_content
def migrate_content!(source_obj, target_obj, opt = {})
parent_kind = opt.fetch(:parent_kind, Voog::API::Contents::ParentKind::Page)
allowed_attr = %i(name content_type)
source.client.contents(parent_kind, source_obj.id, per_page: 250).each do |content|
data = content.to_attrs.select { |k, _| allowed_attr.include?(k) }
new_content = target.client.create_content(parent_kind, target_obj.id, data)
case content.content_type
when 'text'
migrate_text_content!(content, new_content)
when 'form'
migrate_form_content!(content, new_content)
when 'gallery'
migrate_gallery_content!(content, new_content)
when 'content_partial'
migrate_content_partial_content!(content, new_content)
end
end
end
# Migrate text content from source to target host.
#
# Read more: http://www.voog.com/developers/api/resources/texts#update_text
def migrate_text_content!(source_content, target_content)
puts "---> migrate_text_content"
# Update target text
target.client.update_text(target_content.text.id, body: source_content.text.body)
end
# Migrate form content from source to target host.
# NB! Tickets are not migrate!
#
# Read more: http://www.voog.com/developers/api/resources/forms#update_form
def migrate_form_content!(source_content, target_content)
allowed_attr = %i(title submit_label submit_emails submit_email_subject submit_action submit_success_message submit_failure_message submit_success_address fields)
puts "---> migrate form content"
form = source_content.form
data = form.to_attrs.select { |k, _| allowed_attr.include?(k) }.tap do |h|
h[:fields] = form.fields.map(&:to_attrs) # Convert Sawyer::Resource objects to attributes because .to_attrs doesn't convert objects in sub array.
end
# Update target form
target.client.update_form(target_content.form.id, data)
end
# Migrate gallery content from source to target host.
#
# Read more: http://www.voog.com/developers/api/resources/media_sets#update_media_set
def migrate_gallery_content!(source_content, target_content)
allowed_attr = %i(title)
puts "---> migrate gallery content"
# Read more: http://www.voog.com/developers/api/resources/media_sets#update_media_set
gallery = source_content.gallery
if gallery
if source.host == target.host
data = gallery.to_attrs.select { |k, _| allowed_attr.include?(k) }.tap do |h|
h[:assets] = gallery.assets.map { |e| {id: e.id, title: e.title} }
end
# Update target gallery
target.client.update_media_set(target_content.gallery.id, data)
else
# TODO: Copy gallery assets to new host and add to mediaset
assets = gallery.assets.map(&:public_url)
puts "---> WARNING: Gallery images are not migrated!"
puts "----> Images in gallery:\n#{assets.join("\n")}<----\n"
end
end
end
# Migrate content partial content from source to target host.
# Read more: http://www.voog.com/developers/api/resources/content_partials#update_content_partial
def migrate_content_partial_content!(source_content, target_content)
allowed_attr = %i(body metainfo)
puts "---> migrate content partial content"
content_partial = source_content.content_partial.rels[:self].get.data
data = content_partial.to_attrs.select { |k, _| allowed_attr.include?(k) }
# Update target content_partial
target.client.update_content_partial(target_content.content_partial.id, data)
end
def migrate_elements!(source_obj, target_obj)
puts "---> Migrate elements for page '#{source_obj.path}'"
source.paginate(:elements, page_id: source_obj.id, include_values: true).each do |element|
migrate_element!(element, target_obj)
end
end
def migrate_element!(element, target_page)
allowed_attr = %i(title path values)
puts "----> migrate element #{element.id} - '#{element.title}'"
element_definition = target.element_definition_by_title(element.element_definition.title)
if element_definition
data = element.to_attrs.select { |k, _| allowed_attr.include?(k) }.tap do |h|
h[:element_definition_id] = element_definition.id
h[:page_id] = target_page.id
end
new_element = target.client.create_element(data)
# Migrate related content if there has any:
migrate_content!(element, new_element, parent_kind: Voog::API::Contents::ParentKind::Element)
new_element
else
puts "----> SKIPING: Element definition '#{element.element_definition.title}' was not found in target host! Skipping element."
end
end
def migrate_articles!(source_obj, target_obj)
puts "---> Migrate articles for page '#{source_obj.path}'"
source.paginate(:articles, page_id: source_obj.id, include_details: true, include_tags: true).each do |article|
migrate_article!(article, target_obj)
end
end
def migrate_article!(article, target_page)
allowed_attr = %i(path autosaved_title autosaved_excerpt autosaved_body description publishing published created_at tag_names data)
puts "----> migrate article #{article.id} - '#{article.title}'"
# Try to find article author in new host.
author_id = nil
if @source.host == @target.host
allowed_attr.concat(%i(image_id created_by))
else
author = @source.people.detect { |e| e.id = article.author.id }
if author
new_author = @target.people.detect { |e| e.email = author.author }
author_id = new_author.id if new_author
end
end
data = article.to_attrs.select { |k, _| allowed_attr.include?(k) }.tap do |h|
h[:page_id] = target_page.id
h[:created_by] = author_id if author_id
h[:created_at] = article.created_at.strftime('%d.%m.%Y') if article.created_at
end
new_article = target.client.create_article(data)
# Migrate related content if there has any:
migrate_content!(article, new_article, parent_kind: Voog::API::Contents::ParentKind::Article)
new_article
end
# Find all paths that to be migrated.
# TODO: support language codes like "en" in path (root page path vale is empty).
def migratable_paths
@migratable_paths ||= begin
if @paths.empty?
source.page_paths.sort
else
@paths.map { |e| source.page_paths.select { |p| p if p =~ /\A(#{e}|#{e}\/.*)\z/ } }.flatten.uniq.sort
end
end
end
# Find element definitions that are missing in target host.
def migrateble_definitions
@migrateble_definitions ||= source.element_definitions.map(&:title) - target.element_definitions.map(&:title)
end
# Get paths diff between source and target host.
#
# Returns:
# {common: [], source_only: [], target_only: []}
def paths_diff
ensure_cache!
array_diff(source.page_paths, target.page_paths)
end
# Get languages diff between source and target host.
#
# Returns:
# {common: [], source_only: [], target_only: []}
def language_diff
array_diff(source.language_codes, target.language_codes)
end
# Get layout names that are used by pages that are waiting to be migrated.
def required_layouts
@required_layouts ||= migratable_paths.map { |e| source.page_by_path(e).layout.title if source.page_by_path(e) }.compact.uniq.sort
end
# Fetch data and build site trees for source and target hosts.
def ensure_cache!
source.build_site_tree!
target.build_site_tree!
end
# Clear cached data.
def clear_cache!
@migratable_paths = nil
@required_layouts = nil
end
# Helper method to allow different urls mapping between source and target
def get_target_path(path)
if @rewrite_prefixes.empty? || path.to_s.empty?
path
else
slugs = path.split('/')
slugs.size.times do
parent_path = slugs.join('/')
val = @rewrite_prefixes[parent_path]
if val
# Return rewritten value
return path.gsub(/\A#{parent_path}/, val)
else
slugs.pop
end
end
# Rewrite not found - return unchanged path
path
end
end
private
# Get difference between arrays.
def array_diff(a, b)
{
common: (a & b).sort,
source_only: (a - b).sort,
target_only: (b - a).sort
}
end
end
# # Use to reload code in IRB
# def reload!
# load __FILE__
# end
# Run from command line:
if __FILE__ == $0
# Setup Voog API access to source and target host. Required minimal set of parameters:
# source_host - source host name
# source_api_token - source host name
# target_host - source host name
# target_api_token - target api
#
# Optional parameters:
# paths - array of paths. If not give then whole site is migrated otherwise only paths in array are migrate (including sub-pages and parent parent pages (if missing in target)).
# rewrite_prefixes - hash of paths mappings. Allows to change page urls to something different.
# migrate_element_definitions - default "true". If true then all element definition is migrated to target site.
# debug - boolean. Default is "false".
# Example:
#
@conf = {
source_host: 'source-host.voog.com', source_api_token: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', source_protocol: 'http',
target_host: 'target-host.voog.com', target_api_token: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', target_protocol: 'http',
# paths: ['products'],
# rewrite_prefixes: {'products' => 'new-products'}
}
@migrator = VoogSiteMigrator.new(@conf)
puts "Let's start!\n\n"
puts "Source host: #{@migrator.source.host}"
puts "Target host: #{@migrator.target.host}\n"
if ARGV.first == '--migrate=true'
begin
@migrator.migrate_all!
rescue Faraday::ClientError => e
puts "\n\nERROR! Migration was interrupted by error: #{e.message.inspect}"
puts e.response[:body]
end
else
@migrator.run_migration_analyze
puts "\nNow you can run the migrator using command:\n"
puts " ./voog_migrator.rb --migrate=true\n\n"
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment