Last active
March 18, 2019 14:51
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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