contentful-importer 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b73728474014c30ccd464ed1d5cf924709ab305c
4
- data.tar.gz: 09ce5c3def7d50dd13817c754f0e5e8819ca10a5
3
+ metadata.gz: 7bbaffd5d3e9186ca222dc18419f7ed4fa489115
4
+ data.tar.gz: c3d3d73c0700ebc485e27d4f6a078768c5ba6ef1
5
5
  SHA512:
6
- metadata.gz: 1c863bfd32bec39e6c64b437bf84053fa0bd6505e20f42d39a7777a997a7ac575c6ebccf3b7988da8b2b58d692cb4b770af3346c822bdb4222cae2183c9ffed2
7
- data.tar.gz: a76e0c297f18bd96796e859470412a08dbd041f006725c7da4146ceee43cb89d0023c09ac36a109d5d31e6d240b5f9c9937b99249d2b123b1b977ef9e127c135
6
+ metadata.gz: fc7e161a2a8ba9305c8c9a291dbc82d1efa41a8cb65a91cf0191e90f495f238f1b733aba09d79029fbd4591c0901938f9232aee2c60379bc19d5aa3fc3e52630
7
+ data.tar.gz: 5550652c932b12155254e6ef460b657ad76f3ee03daadc6eed800771357f12878bacf3065aa3f8abfaa450c438cdbff10ad179fb7b412d365fd2dfa99d671578
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.1.0
4
+ ### Added
5
+ * Log publish errors similar to import errors
6
+
7
+ ### Fixed
8
+ * Introduce proper namespacing of all classes
9
+
3
10
  ## 0.0.2
4
11
  ### Added
5
12
  * Read contentType and description from asset JSON files, falling back to contentType inference from the asset URL.
data/README.md CHANGED
@@ -63,8 +63,8 @@ contentful-importer --config-file settings.yml --action
63
63
  contentful_structure_dir: PATH_TO_CONTENTFUL_STRUCTURE_JSON_FILE
64
64
 
65
65
  ## CONVERT CONTENTFUL MODEL TO CONTENTFUL IMPORT STRUCTURE
66
- content_model_json:
67
- converted_model_dir:
66
+ content_model_json: PATH_TO_CONTENTFUL_MODEL_JSON_FILE ## for input
67
+ converted_model_dir: PATH_TO_CONTENTFUL_MODEL_JSON_FILE ## for output
68
68
  ```
69
69
 
70
70
  2. Create the contentful_structure.json. First you need to create a content model using the [Contentful web application](www.contentful.com). Then you can download the content model using the content management api and use the content model for the import:
@@ -140,6 +140,7 @@ To display all actions use the `-h` option:
140
140
  ```bash
141
141
  contentful-importer -h
142
142
  ```
143
+
143
144
  #### --convert-content-model-to-json
144
145
 
145
146
  If you already have an existing content model for a space it can be downloaded and used for the import:
@@ -150,7 +151,6 @@ If you already have an existing content model for a space it can be downloaded a
150
151
  'https://api.contentful.com/spaces/SPACE_ID/content_types' > contentful_model.json
151
152
  ```
152
153
 
153
-
154
154
  In the **settings.yml** specify the PATH to **contentful_model.json**.
155
155
 
156
156
  ```yaml
@@ -222,6 +222,7 @@ To publish all entries:
222
222
  ```bash
223
223
  contentful-importer --config-file settings.yml --publish-entries
224
224
  ```
225
+
225
226
  Number of threads that are used in the publishing of entries is dependent on `--threads` argument, which you specified when import data.
226
227
 
227
228
  #### --publish-assets ARGS
@@ -231,6 +232,7 @@ You can publish all assets with single Thread:
231
232
  ```bash
232
233
  contentful-importer --config-file settings.yml --publish-assets
233
234
  ```
235
+
234
236
  or add ```--threads``` argument to use multiple Threads:
235
237
 
236
238
  ```bash
@@ -310,7 +312,7 @@ Before you start importing the content make sure you read [how to use it](https:
310
312
 
311
313
  #### Space ID
312
314
 
313
- After [importing the content types](https://github.com/contentful/generic-importer.rb#--import-content-types-args) to the Space, you need to specify the `space_id` parameter in the settings.
315
+ After [importing the content types](https://github.com/contentful/generic-importer.rb#--import-content-types-args) to the Space, you need to specify the `space_id` parameter in the settings. Please note that the content-type import only accepts the space ID as a commandline option.
314
316
 
315
317
 
316
318
  Example:
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'escort'
4
- require_relative '../lib/cli'
4
+ require_relative '../lib/contentful/importer/cli'
5
5
 
6
6
  fail ArgumentError, 'Set the path to the configuration file and define an action. More information can be found in the README.' if ARGF.argv.empty?
7
7
  fail ArgumentError, "Missing '--config-file' argument. Usage: 'contentful-importer --config-file PATH_TO_CONFIGURATION_FILE --action'." unless ARGV.include?('--config-file')
@@ -15,13 +15,13 @@ Escort::App.create do |app|
15
15
  app.command '--create-contentful-model-from-json' do |command|
16
16
  command.summary 'Create content types json files, based on contentful_structure.json file.'
17
17
  command.action do |options, arguments|
18
- Command::CLI.new(options, arguments).execute
18
+ Contentful::Importer::CLI.new(options, arguments).execute
19
19
  end
20
20
  end
21
21
  app.command '--convert-content-model-to-json' do |command|
22
22
  command.summary 'Transform contentful model to contentful structure ready to import. View README'
23
23
  command.action do |options, arguments|
24
- Command::CLI.new(options, arguments).execute
24
+ Contentful::Importer::CLI.new(options, arguments).execute
25
25
  end
26
26
  end
27
27
  app.command '--import-content-types' do |command|
@@ -32,7 +32,7 @@ Escort::App.create do |app|
32
32
  opts.opt :space_name, 'space_name', short: :none, long: '--space_name', type: :string
33
33
  end
34
34
  command.action do |options, arguments|
35
- Command::CLI.new(options, arguments).execute
35
+ Contentful::Importer::CLI.new(options, arguments).execute
36
36
  end
37
37
  end
38
38
  app.command '--import' do |command|
@@ -42,26 +42,26 @@ Escort::App.create do |app|
42
42
  opts.validate(:threads, 'argument must be set to 1 or 2 exclusive. ') { |option| option > 0 && option <= 2 }
43
43
  end
44
44
  command.action do |options, arguments|
45
- Command::CLI.new(options, arguments).execute
45
+ Contentful::Importer::CLI.new(options, arguments).execute
46
46
  end
47
47
  end
48
48
  app.command '--test-credentials' do |command|
49
49
  command.summary 'Check your Contentful-management credentials'
50
50
  command.action do |options, arguments|
51
- Command::CLI.new(options, arguments).execute
51
+ Contentful::Importer::CLI.new(options, arguments).execute
52
52
  end
53
53
  end
54
54
  app.command '--import-assets' do |command|
55
55
  command.summary 'Import only assets'
56
56
  command.action do |options, arguments|
57
- Command::CLI.new(options, arguments).execute
57
+ Contentful::Importer::CLI.new(options, arguments).execute
58
58
  end
59
59
  end
60
60
 
61
61
  app.command '--publish-entries' do |command|
62
62
  command.summary 'Publish all entries'
63
63
  command.action do |options, arguments|
64
- Command::CLI.new(options, arguments).execute
64
+ Contentful::Importer::CLI.new(options, arguments).execute
65
65
  end
66
66
  end
67
67
 
@@ -72,13 +72,13 @@ Escort::App.create do |app|
72
72
  opts.validate(:threads, 'argument must be set to 1 or 2 exclusive. ') { |option| option > 0 && option <= 2 }
73
73
  end
74
74
  command.action do |options, arguments|
75
- Command::CLI.new(options, arguments).execute
75
+ Contentful::Importer::CLI.new(options, arguments).execute
76
76
  end
77
77
  end
78
78
  app.command '--validate-schema' do |command|
79
79
  command.summary 'Validate entries JSON schema'
80
80
  command.action do |options, arguments|
81
- Command::CLI.new(options, arguments).execute
81
+ Contentful::Importer::CLI.new(options, arguments).execute
82
82
  end
83
83
  end
84
84
  end
@@ -2,11 +2,11 @@
2
2
  lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
- require File.expand_path('../lib/version', __FILE__)
5
+ require File.expand_path('../lib/contentful/importer/version', __FILE__)
6
6
 
7
7
  Gem::Specification.new do |spec|
8
8
  spec.name = 'contentful-importer'
9
- spec.version = Version::VERSION
9
+ spec.version = Contentful::Importer::VERSION
10
10
  spec.authors = ['Contentful GmbH (Andreas Tiefenthaler)']
11
11
  spec.email = ['rubygems@contentful.com']
12
12
  spec.description = 'Generic importer for contentful.com'
@@ -0,0 +1,15 @@
1
+ require_relative 'migrator'
2
+ require 'yaml'
3
+
4
+ module Contentful
5
+ module Importer
6
+ class CLI < Escort::ActionCommand::Base
7
+
8
+ def execute
9
+ setting_file = YAML.load_file(global_options[:file])
10
+ Migrator.new(setting_file).run(command_name, command_options)
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,38 @@
1
+ require 'active_support/core_ext/hash'
2
+ module Contentful
3
+ module Importer
4
+ class Configuration
5
+ attr_reader :space_id,
6
+ :config,
7
+ :data_dir,
8
+ :collections_dir,
9
+ :entries_dir,
10
+ :assets_dir,
11
+ :log_files_dir,
12
+ :threads_dir,
13
+ :imported_entries,
14
+ :published_entries,
15
+ :published_assets,
16
+ :space_id
17
+
18
+ def initialize(settings)
19
+ @config = settings
20
+ validate_required_parameters
21
+ @data_dir = settings['data_dir']
22
+ @collections_dir = "#{data_dir}/collections"
23
+ @entries_dir = "#{data_dir}/entries"
24
+ @assets_dir = "#{data_dir}/assets"
25
+ @log_files_dir = "#{data_dir}/logs"
26
+ @threads_dir = "#{data_dir}/threads"
27
+ @imported_entries = []
28
+ @published_entries = []
29
+ @published_assets = []
30
+ @space_id = settings['space_id']
31
+ end
32
+
33
+ def validate_required_parameters
34
+ fail ArgumentError, 'Set PATH to data_dir. Folder where all data will be stored. View README' if config['data_dir'].nil?
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,5 +1,5 @@
1
1
  module Contentful
2
- module Converter
2
+ module Importer
3
3
  class ContentTypesStructureCreator
4
4
 
5
5
  attr_reader :config, :logger
@@ -1,7 +1,7 @@
1
1
  require_relative 'content_types_structure_creator'
2
2
 
3
3
  module Contentful
4
- module Converter
4
+ module Importer
5
5
  class ContentfulModelToJson
6
6
  attr_reader :config, :logger, :converted_model_dir, :content_types
7
7
 
@@ -0,0 +1,119 @@
1
+ require 'fileutils'
2
+ require 'thread'
3
+ require_relative 'parallel_importer'
4
+
5
+ module Contentful
6
+ module Importer
7
+ class DataOrganizer
8
+
9
+ attr_reader :config, :split_params, :logger
10
+
11
+ def initialize(settings)
12
+ @config = settings
13
+ @split_params = {object_index: 0, current_thread: 0}
14
+ @logger = Logger.new(STDOUT)
15
+ end
16
+
17
+ def execute(threads_count)
18
+ create_threads_subdirectories(threads_count, true)
19
+ split_entries(threads_count)
20
+ end
21
+
22
+ def split_assets_to_threads(threads_count)
23
+ create_threads_subdirectories(threads_count, false, {assets: 'assets/'})
24
+ split_assets(threads_count)
25
+ end
26
+
27
+ def split_entries(threads_count)
28
+ entries_per_thread_count = total_entries_count / threads_count
29
+ Dir.glob("#{config.entries_dir}/*") do |dir_path|
30
+ collection_name = File.basename(dir_path)
31
+ if has_contentful_structure?(collection_name)
32
+ content_type_id = content_type_id_from_file(collection_name)
33
+ process_collection_files(content_type_id, dir_path, entries_per_thread_count, threads_count)
34
+ end
35
+ end
36
+ end
37
+
38
+ def split_assets(threads_count)
39
+ asset_per_thread = total_assets_count / threads_count
40
+ Dir.glob("#{config.assets_dir}/**/*json") do |asset_path|
41
+ copy_asset(asset_path)
42
+ split_params[:object_index] += 1
43
+ count_index_files(asset_per_thread, threads_count)
44
+ end
45
+ end
46
+
47
+ def process_collection_files(content_type_id, dir_path, entries_per_thread_count, threads_count)
48
+ logger.info "Processing collection: #{content_type_id}"
49
+ Dir.glob("#{dir_path}/*.json") do |entry_path|
50
+ copy_entry(entry_path, split_params[:current_thread], content_type_id)
51
+ split_params[:object_index] += 1
52
+ count_index_files(entries_per_thread_count, threads_count)
53
+ end
54
+ end
55
+
56
+ def count_index_files(objects_per_thread_count, threads_count)
57
+ if split_params[:object_index] == objects_per_thread_count
58
+ split_params[:object_index] = 0
59
+ set_current_thread(threads_count)
60
+ end
61
+ end
62
+
63
+ def set_current_thread(threads_count)
64
+ split_params[:current_thread] += 1
65
+ split_params[:current_thread] = 0 if split_params[:current_thread] == threads_count
66
+ end
67
+
68
+ def has_contentful_structure?(collection_file)
69
+ File.exist?("#{config.collections_dir}/#{collection_file}.json")
70
+ end
71
+
72
+ def content_type_id_from_file(collection_file)
73
+ JSON.parse(File.read("#{config.collections_dir}/#{collection_file}.json"))['id']
74
+ end
75
+
76
+ def new_entry_name(content_type_id, entry_path)
77
+ "#{content_type_id}_#{File.basename(entry_path, '.*').match(/(\d+)/)[0]}.json"
78
+ end
79
+
80
+ def copy_entry(entry_path, current_thread, content_type_id)
81
+ FileUtils.cp entry_path, "#{config.threads_dir}/#{current_thread}/#{new_entry_name(content_type_id, entry_path)}"
82
+ end
83
+
84
+ def copy_asset(asset_path)
85
+ FileUtils.cp asset_path, "#{config.threads_dir}/assets/#{split_params[:current_thread]}/#{File.basename(asset_path)}"
86
+ end
87
+
88
+ def create_threads_subdirectories(threads_count, validate, dir = {})
89
+ validate_collections_files if validate
90
+ create_directory(config.threads_dir)
91
+ threads_count.times do |thread_id|
92
+ create_directory("#{config.threads_dir}/#{dir[:assets]}#{thread_id}")
93
+ end
94
+ end
95
+
96
+ def create_directory(path)
97
+ FileUtils.mkdir_p(path) unless File.directory?(path)
98
+ end
99
+
100
+ def total_entries_count
101
+ total_number = 0
102
+ Dir.glob("#{config.entries_dir}/*") do |dir_path|
103
+ collection_name = File.basename(dir_path)
104
+ total_number += Dir.glob("#{config.entries_dir}/#{collection_name}/*").count if has_contentful_structure?(collection_name)
105
+ end
106
+ total_number
107
+ end
108
+
109
+ def total_assets_count
110
+ Dir.glob("#{config.assets_dir}/**/*json").count
111
+ end
112
+
113
+ def validate_collections_files
114
+ fail ArgumentError, "Make sure the #{config.collections_dir} directory exists and the content structure resides within it. View README" unless Dir.exist?(config.collections_dir)
115
+ fail ArgumentError, 'Collections directory is empty! Create content types JSON files. View README' if Dir.glob("#{config.collections_dir}/*").empty?
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,66 @@
1
+ require 'json-schema'
2
+
3
+ module Contentful
4
+ module Importer
5
+ class JsonSchemaValidator
6
+
7
+ attr_reader :config, :logger
8
+
9
+ def initialize(configuration)
10
+ @config = configuration
11
+ @logger = Logger.new(STDOUT)
12
+ end
13
+
14
+ def validate_schemas
15
+ Dir.glob("#{config.collections_dir}/*") do |content_type_file|
16
+ validate_schema(content_type_file)
17
+ end
18
+ end
19
+
20
+ def validate_schema(content_type_file)
21
+ schema = parse_content_type_schema(JSON.parse(File.read(content_type_file)))
22
+ content_type_filename = File.basename(content_type_file, '.*')
23
+ validate_entry(content_type_filename, schema)
24
+ end
25
+
26
+ def validate_entry(content_type_filename, schema)
27
+ Dir.glob("#{config.entries_dir}/#{content_type_filename}/*") do |entry_file|
28
+ entry_schema = JSON.parse(File.read(entry_file))
29
+ begin
30
+ JSON::Validator.validate!(schema, entry_schema)
31
+ rescue JSON::Schema::ValidationError => error
32
+ logger.info "#{error.message}! Path to invalid entry: #{entry_file}"
33
+ end
34
+ end
35
+ end
36
+
37
+ def parse_content_type_schema(ct_file)
38
+ new_hash = base_schema_format
39
+ ct_file['fields'].each do |key|
40
+ type = convert_type(key['type'])
41
+ new_hash['properties'].merge!({key['id'] => {'type' => type}})
42
+ end
43
+ new_hash
44
+ end
45
+
46
+ def base_schema_format
47
+ {'type' => 'object', 'properties' => {}}
48
+ end
49
+
50
+ def convert_type(type)
51
+ case type
52
+ when 'Text', 'Date', 'Symbol'
53
+ 'string'
54
+ when 'Number'
55
+ 'float'
56
+ when 'Asset', 'Entry'
57
+ 'object'
58
+ else
59
+ type.downcase
60
+ end
61
+ end
62
+
63
+ end
64
+ end
65
+ end
66
+
@@ -0,0 +1,43 @@
1
+ require_relative 'parallel_importer'
2
+ require_relative 'configuration'
3
+ require_relative 'converters/contentful_model_to_json'
4
+ require_relative 'json_schema_validator'
5
+
6
+ module Contentful
7
+ module Importer
8
+ class Migrator
9
+
10
+ attr_reader :importer, :converter, :config, :json_validator
11
+
12
+ def initialize(settings)
13
+ @config = Configuration.new(settings)
14
+ @importer = ParallelImporter.new(@config)
15
+ @converter = ContentfulModelToJson.new(@config)
16
+ @json_validator = JsonSchemaValidator.new(@config)
17
+ end
18
+
19
+ def run(action, options = {})
20
+ case action.to_s
21
+ when '--create-contentful-model-from-json'
22
+ converter.create_content_type_json
23
+ when '--import-content-types'
24
+ importer.create_contentful_model(options)
25
+ when '--import'
26
+ importer.import_data(options[:threads])
27
+ when '--convert-content-model-to-json'
28
+ converter.convert_to_import_form
29
+ when '--publish-entries'
30
+ importer.publish_entries_in_threads
31
+ when '--test-credentials'
32
+ importer.test_credentials
33
+ when '--import-assets'
34
+ importer.import_only_assets
35
+ when '--publish-assets'
36
+ importer.publish_assets_in_threads(options[:threads])
37
+ when '--validate-schema'
38
+ json_validator.validate_schemas
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end