sutty-migration 0.3.4 → 0.4.0rc0
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.
- checksums.yaml +4 -4
- data/README.md +16 -15
- data/lib/jekyll-wordpress-sql-migration.rb +132 -0
- data/lib/jekyll-wordpress-xml-migration.rb +19 -0
- data/lib/sutty_migration/data.rb +3 -3
- data/lib/sutty_migration/wordpress.rb +6 -52
- data/lib/sutty_migration/wordpress_xml/attachment.rb +71 -10
- data/lib/sutty_migration/wordpress_xml/post.rb +107 -12
- data/lib/sutty_migration/wordpress_xml.rb +12 -7
- metadata +70 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 33b04f68ec23be8ad61143db87c0496fc1a01a84fdb3351b9f76a2782f79342f
|
|
4
|
+
data.tar.gz: 411e0050ee48d300c7bcda874f1943b33d867f4c090f72650c0122a69027faa2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 708dff82c233c3819acbe9ccf3901d8345463282ba9df953adb8ac0f15f9646560e514c87499921d13ac6bfa2693c71b0d634e03e1f04e87d0e148174bd91c50
|
|
7
|
+
data.tar.gz: 0faa1dc6c107f0999d8a52b7c28b08ad1540e44cff8fd488bfcfae55d2d22548fe0afe61fb54520f4b1d8552674079ba65d617a361139cbb1a5c761581e6df7f
|
data/README.md
CHANGED
|
@@ -91,30 +91,31 @@ SQLite syntax, since `mysql2sqlite` doesn't support this yet.
|
|
|
91
91
|
Wordpress websites can include lots of posts and metadata, depending on
|
|
92
92
|
the amount of plugins installed. We don't have an official way of
|
|
93
93
|
dumping everything into Jekyll, because you will probably want to move
|
|
94
|
-
things around. You can write a plugin like this
|
|
94
|
+
things around. You can write a plugin like this to inspect what's
|
|
95
|
+
happening on every stage:
|
|
95
96
|
|
|
96
97
|
```ruby
|
|
97
98
|
# _plugins/wordpress.rb
|
|
98
99
|
# frozen_string_literal: true
|
|
99
100
|
|
|
100
|
-
require '
|
|
101
|
-
require '
|
|
102
|
-
require 'jekyll-write-and-commit-changes'
|
|
103
|
-
|
|
104
|
-
Jekyll::Hooks.register :site, :post_read, priority: :low do |site|
|
|
105
|
-
wp = SuttyMigration::Wordpress.new(site: site, database: 'wordpress.sqlite3', prefix: 'wp_', url: 'https://wordpre.ss')
|
|
106
|
-
|
|
107
|
-
# Download all files
|
|
108
|
-
wp.download_all
|
|
101
|
+
require 'jekyll-wordpress-sql-migration'
|
|
102
|
+
require 'pry'
|
|
109
103
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
doc.content = post[:content]
|
|
113
|
-
doc.save
|
|
114
|
-
end
|
|
104
|
+
Jekyll::Hooks.register :wordpress_post, :post_init do |item|
|
|
105
|
+
binding.pry
|
|
115
106
|
end
|
|
116
107
|
```
|
|
117
108
|
|
|
109
|
+
Available hooks are `:wordpress_attachment`, `:wordpress_post` and
|
|
110
|
+
`:migration` for the `:post_init`, `:pre_render`, `:post_convert`,
|
|
111
|
+
`:post_render` and `:post_write` stages.
|
|
112
|
+
|
|
113
|
+
And then set `JEKYLL_ENV=migration` for the migration to run.
|
|
114
|
+
|
|
115
|
+
If the website is still available (although probably doesn't),
|
|
116
|
+
attachments will be downloaded to `wp-content/`. If you have them from
|
|
117
|
+
a backup, put them there so the plugin finds them.
|
|
118
|
+
|
|
118
119
|
### WordPress XML
|
|
119
120
|
|
|
120
121
|
If you have the XML dump from a WordPress site, you can migrate content
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Enable this plugin if you have an SQL dump backup of your Wordpress
|
|
4
|
+
# site. To avoid having to run a MySQL/MariaDB database, we convert the
|
|
5
|
+
# dump into an SQLite database with our fork of
|
|
6
|
+
# [mysql2sqlite](https://git.sutty.abyaya.la/misc/mysql2sqlite)
|
|
7
|
+
#
|
|
8
|
+
# Put the SQLite file on _files/wordpress.sqlite3 and set the URL on the
|
|
9
|
+
# `url` field on `_config.yml`. If the new website doesn't match the
|
|
10
|
+
# URL, you can use `wordpress_url`.
|
|
11
|
+
#
|
|
12
|
+
# If the prefix for Wordpress tables is different than "wp_", set
|
|
13
|
+
# `wordpress_prefix` on `_config.yml`.
|
|
14
|
+
#
|
|
15
|
+
# ```sh
|
|
16
|
+
# JEKYLL_ENV=migration bundle exec jekyll build
|
|
17
|
+
# ```
|
|
18
|
+
#
|
|
19
|
+
# ## Hooks
|
|
20
|
+
#
|
|
21
|
+
# Hook to `:migration` if you need to access and modify data during the
|
|
22
|
+
# migration process:
|
|
23
|
+
#
|
|
24
|
+
# * `:post_init`: will receive the `Hash`
|
|
25
|
+
# * `:pre_render`: will receive the `SuttyMigration::WordpressXml::Post`
|
|
26
|
+
# * `:post_convert`: will receive the `Jekyll::Document` after setting front matter
|
|
27
|
+
# * `:post_render`: will receive the `Jekyll::Document` after sanitizing the content
|
|
28
|
+
# * `:post_write`: will receive the `Jekyll::Document` after writing to disk
|
|
29
|
+
|
|
30
|
+
Jekyll::Hooks.register :site, :post_read, priority: :high do |site|
|
|
31
|
+
next unless Jekyll.env == 'migration'
|
|
32
|
+
|
|
33
|
+
database = '_files/wordpress.sqlite3'
|
|
34
|
+
|
|
35
|
+
Jekyll.logger.abort_with 'Migration:', "#{database} doesn't exist!" unless File.exist? database
|
|
36
|
+
|
|
37
|
+
require 'sutty_migration/wordpress'
|
|
38
|
+
require 'sutty_migration/wordpress_xml/post'
|
|
39
|
+
require 'sutty_migration/wordpress_xml/attachment'
|
|
40
|
+
require 'sutty_migration/jekyll/document_creator'
|
|
41
|
+
require 'jekyll-write-and-commit-changes'
|
|
42
|
+
|
|
43
|
+
url = site.config['wordpress_url'] || site.config['url']
|
|
44
|
+
prefix = site.config['wordpress_prefix'] || 'wp_'
|
|
45
|
+
|
|
46
|
+
wordpress = SuttyMigration::Wordpress.new(site:, url:, database:, prefix:)
|
|
47
|
+
migrated_posts = []
|
|
48
|
+
|
|
49
|
+
Jekyll::Hooks.trigger :migration, :post_init, wordpress
|
|
50
|
+
|
|
51
|
+
wordpress.posts(layout: 'attachment').each do |item|
|
|
52
|
+
Jekyll::Hooks.trigger :wordpress_attachment, :post_init, item
|
|
53
|
+
|
|
54
|
+
attachment = SuttyMigration::WordpressXml::Attachment.new(wordpress:, item:)
|
|
55
|
+
|
|
56
|
+
# Download and convert to Sutty's public/
|
|
57
|
+
Jekyll::Hooks.trigger :wordpress_attachment, :pre_render, attachment
|
|
58
|
+
# Index attachments by id, public path and original destination so
|
|
59
|
+
# we can find them later.
|
|
60
|
+
wordpress.attachments[attachment.attachment_url] =
|
|
61
|
+
wordpress.attachments[attachment.public_path] =
|
|
62
|
+
wordpress.attachments[item[:id]] =
|
|
63
|
+
attachment
|
|
64
|
+
Jekyll::Hooks.trigger :wordpress_attachment, :post_convert, attachment
|
|
65
|
+
Jekyll::Hooks.trigger :wordpress_attachment, :post_render, attachment
|
|
66
|
+
|
|
67
|
+
Jekyll::Hooks.trigger :wordpress_attachment, :post_write, attachment
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
Jekyll::Hooks.trigger :migration, :pre_render, wordpress
|
|
71
|
+
|
|
72
|
+
# We have to instantiate Posts from a Hash instead of an XML object.
|
|
73
|
+
wordpress.posts(layout: 'post').each do |item|
|
|
74
|
+
Jekyll::Hooks.trigger :wordpress_post, :post_init, item
|
|
75
|
+
|
|
76
|
+
item[:title] = 'Draft' if item[:title].empty?
|
|
77
|
+
item[:slug] = Jekyll::Utils.slugify(item[:title], mode: 'latin') if item[:slug].empty?
|
|
78
|
+
|
|
79
|
+
post = SuttyMigration::WordpressXml::Post.new(wordpress:, item:).tap do |p|
|
|
80
|
+
p.categories = [item.dig(:terms, :category)].flatten.compact
|
|
81
|
+
p.tags = [item.dig(:terms, :post_tag)].flatten.compact
|
|
82
|
+
p.date = item[:date] || item[:last_modified_at]
|
|
83
|
+
p.last_modified_at = item[:last_modified_at]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
Jekyll::Hooks.trigger :wordpress_post, :pre_render, post
|
|
87
|
+
|
|
88
|
+
Jekyll::Document.find_or_create(site:, slug: post.slug, title: post.title, date: post.date,
|
|
89
|
+
collection: 'posts').tap do |doc|
|
|
90
|
+
# If you want to customize the layout, change it with a :post_init
|
|
91
|
+
# hook.
|
|
92
|
+
doc.data['layout'] = item[:layout]
|
|
93
|
+
doc.data['created_at'] = post.date
|
|
94
|
+
doc.data['last_modified_at'] = post.last_modified_at&.localtime
|
|
95
|
+
doc.data['uuid'] ||= SecureRandom.uuid
|
|
96
|
+
doc.data['categories'] = post.categories
|
|
97
|
+
doc.data['tags'] = post.tags
|
|
98
|
+
doc.data['draft'] = post.draft?
|
|
99
|
+
|
|
100
|
+
unless (attachment_id = item.dig(:front_matter, :_thumbnail_id).to_i).zero?
|
|
101
|
+
attachment = wordpress.attachments[attachment_id]
|
|
102
|
+
|
|
103
|
+
if attachment
|
|
104
|
+
doc.data['image'] = {
|
|
105
|
+
'path' => attachment.public_path,
|
|
106
|
+
'description' => ''
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
Jekyll::Hooks.trigger :wordpress_post, :post_convert, doc
|
|
112
|
+
|
|
113
|
+
doc.content = post.beautiful_content
|
|
114
|
+
|
|
115
|
+
Jekyll::Hooks.trigger :wordpress_post, :post_render, doc
|
|
116
|
+
|
|
117
|
+
migrated_posts << doc
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
Jekyll::Hooks.trigger :migration, :post_convert, wordpress
|
|
122
|
+
Jekyll::Hooks.trigger :migration, :post_render, migrated_posts
|
|
123
|
+
|
|
124
|
+
# Save them all at the end
|
|
125
|
+
migrated_posts.each do |doc|
|
|
126
|
+
doc.save
|
|
127
|
+
|
|
128
|
+
Jekyll::Hooks.trigger :wordpress_post, :post_write, doc
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
Jekyll::Hooks.trigger :migration, :post_write, migrated_posts
|
|
132
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Enable this plugin if you have an XML backup of your Wordpress site.
|
|
4
|
+
# Although this is the easiest path, it's not always possible to have
|
|
5
|
+
# one, either because the website is not working or producing the XML
|
|
6
|
+
# dump gets canceled by PHP resource usage limits.
|
|
7
|
+
#
|
|
8
|
+
# Put the XML file on _files/wordpress.xml and set the URL on the `url`
|
|
9
|
+
# field on _config.yml. If the new website doesn't match the URL, you
|
|
10
|
+
# can use `wordpress_url`.
|
|
11
|
+
#
|
|
12
|
+
# JEKYLL_ENV=migration bundle exec jekyll build
|
|
13
|
+
Jekyll::Hooks.register :site, :post_read, priority: :high do |_site|
|
|
14
|
+
next unless Jekyll.env == 'migration'
|
|
15
|
+
|
|
16
|
+
wordpress_xml = '_files/wordpress.xml'
|
|
17
|
+
|
|
18
|
+
Jekyll.logger.abort_with 'Migration:', "#{wordpress_xml} is missing!" unless File.exists wordpress_xml
|
|
19
|
+
end
|
data/lib/sutty_migration/data.rb
CHANGED
|
@@ -27,9 +27,9 @@ Jekyll::Hooks.register :site, :post_read, priority: :low do |site|
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
document ||= begin
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
data = row.slice(*%w[date slug title]).transform_keys(&:to_sym)
|
|
31
|
+
Jekyll::Document.find_or_create(site: site, collection: 'posts', **data)
|
|
32
|
+
end
|
|
33
33
|
next unless document
|
|
34
34
|
|
|
35
35
|
row.each do |attribute, value|
|
|
@@ -17,7 +17,7 @@ module SuttyMigration
|
|
|
17
17
|
# migration plugin where you can convert data by yourself. We may add
|
|
18
18
|
# this feature in the future.
|
|
19
19
|
class Wordpress
|
|
20
|
-
attr_reader :site, :prefix, :limit, :url, :wp, :database, :multisite
|
|
20
|
+
attr_reader :site, :prefix, :limit, :url, :wp, :database, :multisite, :attachments
|
|
21
21
|
|
|
22
22
|
# @param :site [Jekyll::Site] Jekyll site
|
|
23
23
|
# @param :url [String] Wordpress site URL (must be up for downloads)
|
|
@@ -32,13 +32,14 @@ module SuttyMigration
|
|
|
32
32
|
@url = url.freeze
|
|
33
33
|
@database = database || File.join(site.source, '_data', 'wordpress.sqlite3')
|
|
34
34
|
@multisite = multisite
|
|
35
|
+
@attachments = {}
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
# Generate database connections for a multisite WP
|
|
38
39
|
#
|
|
39
40
|
# @return [Hash] { "ID" => SuttyMigration::Wordpress }
|
|
40
41
|
def blogs
|
|
41
|
-
@blogs ||= wp["select * from #{prefix}blogs"].to_a.
|
|
42
|
+
@blogs ||= wp["select * from #{prefix}blogs"].to_a.to_h do |blog|
|
|
42
43
|
url = "https://#{blog[:domain]}#{blog[:path]}"
|
|
43
44
|
pfx = "#{prefix}#{blog[:blog_id]}_" if blog[:blog_id] > 1
|
|
44
45
|
pfx ||= prefix
|
|
@@ -46,11 +47,11 @@ module SuttyMigration
|
|
|
46
47
|
[blog[:blog_id],
|
|
47
48
|
blog.merge(db: self.class.new(site: site, url: url, prefix: pfx, database: database, limit: limit,
|
|
48
49
|
multisite: self))]
|
|
49
|
-
end
|
|
50
|
+
end
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
def options
|
|
53
|
-
@options ||= wp["select option_name, option_value from #{prefix}options"].to_a.
|
|
54
|
+
@options ||= wp["select option_name, option_value from #{prefix}options"].to_a.to_h(&:values).transform_keys(&:to_sym)
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
# Open the database.
|
|
@@ -62,53 +63,6 @@ module SuttyMigration
|
|
|
62
63
|
end
|
|
63
64
|
end
|
|
64
65
|
|
|
65
|
-
# Download all attachments. Adds the local path to them.
|
|
66
|
-
#
|
|
67
|
-
# @param :progress [Boolean] Toggle progress bar
|
|
68
|
-
# @return [Nil]
|
|
69
|
-
def download_all(progress: true)
|
|
70
|
-
posts(layout: 'attachment').each do |attachment|
|
|
71
|
-
attachment[:front_matter]['file_path'] = download(url: attachment[:guid], progress: progress)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Downloads a file if needed, optionally showing a progress bar.
|
|
76
|
-
#
|
|
77
|
-
# @param :url [String] File URL
|
|
78
|
-
# @param :progress [Boolean] Toggle progress bar
|
|
79
|
-
# @return [String] File local path
|
|
80
|
-
def download(url:, progress: true)
|
|
81
|
-
uri = URI(url)
|
|
82
|
-
dest = uri.path.sub(%r{\A/}, '')
|
|
83
|
-
full = File.join(site.source, dest)
|
|
84
|
-
|
|
85
|
-
return dest if File.exist? full
|
|
86
|
-
|
|
87
|
-
::Jekyll.logger.info "Downloading #{dest}"
|
|
88
|
-
|
|
89
|
-
FileUtils.mkdir_p File.dirname(full)
|
|
90
|
-
|
|
91
|
-
File.open(full, 'w') do |f|
|
|
92
|
-
if progress
|
|
93
|
-
head = Faraday.head(url)
|
|
94
|
-
content_length = head.headers['content-length'].to_i
|
|
95
|
-
progress = ProgressBar.create(title: File.basename(dest), total: content_length, output: $stderr)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
Faraday.get(url) do |req|
|
|
99
|
-
req.options.on_data = proc do |chunk, downloaded_bytes|
|
|
100
|
-
f.write chunk
|
|
101
|
-
|
|
102
|
-
if progress
|
|
103
|
-
progress.progress = downloaded_bytes > content_length ? content_length : downloaded_bytes
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
dest
|
|
110
|
-
end
|
|
111
|
-
|
|
112
66
|
# List post types
|
|
113
67
|
#
|
|
114
68
|
# @return [Array]
|
|
@@ -129,7 +83,7 @@ module SuttyMigration
|
|
|
129
83
|
end
|
|
130
84
|
|
|
131
85
|
wp[post_query(**options)].each_page(limit).to_a.map(&:to_a).flatten.tap do |p|
|
|
132
|
-
p.
|
|
86
|
+
p.each do |post|
|
|
133
87
|
# Sequel parses dates on localtime
|
|
134
88
|
post[:date] = ::Jekyll::Utils.parse_date(post[:date]) unless post[:date].blank?
|
|
135
89
|
unless post[:last_modified_at].blank?
|
|
@@ -4,6 +4,7 @@ require_relative 'post'
|
|
|
4
4
|
require 'php-serialize'
|
|
5
5
|
require 'faraday'
|
|
6
6
|
require 'progressbar'
|
|
7
|
+
require 'addressable'
|
|
7
8
|
|
|
8
9
|
module SuttyMigration
|
|
9
10
|
class WordpressXml
|
|
@@ -13,14 +14,14 @@ module SuttyMigration
|
|
|
13
14
|
#
|
|
14
15
|
# @return [String]
|
|
15
16
|
def attachment_url
|
|
16
|
-
@attachment_url ||= attribute_value
|
|
17
|
+
@attachment_url ||= attribute_value('attachment_url') || attribute_value('guid')
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
# File destination
|
|
20
21
|
#
|
|
21
22
|
# @return [String]
|
|
22
23
|
def dest
|
|
23
|
-
@dest ||= CGI.unescape(URI(attachment_url).path.sub(%r{\A/}, ''))
|
|
24
|
+
@dest ||= CGI.unescape(Addressable::URI.parse(attachment_url).path.sub(%r{\A/}, '').unicode_normalize)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
# Metadata, with file information as a Hash
|
|
@@ -45,25 +46,85 @@ module SuttyMigration
|
|
|
45
46
|
FileUtils.mkdir_p File.dirname(dest)
|
|
46
47
|
|
|
47
48
|
File.open(dest, 'w') do |f|
|
|
49
|
+
head = Faraday.head(attachment_url)
|
|
50
|
+
|
|
51
|
+
if head.status > 399
|
|
52
|
+
::Jekyll.logger.warn 'Error:', head.status.to_s
|
|
53
|
+
return false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
content_length = head.headers['content-length'].to_i
|
|
57
|
+
|
|
58
|
+
if content_length.zero?
|
|
59
|
+
::Jekyll.logger.warn 'Error:', 'Content length is zero'
|
|
60
|
+
return false
|
|
61
|
+
end
|
|
62
|
+
|
|
48
63
|
if progress
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
progress = ProgressBar.create(title: File.basename(dest), total: content_length, output: $stderr)
|
|
64
|
+
progress_bar = ProgressBar.create(title: File.basename(dest), total: content_length,
|
|
65
|
+
output: $stderr)
|
|
52
66
|
end
|
|
53
67
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
begin
|
|
69
|
+
Faraday.get(attachment_url) do |req|
|
|
70
|
+
req.options.on_data = proc do |chunk, downloaded_bytes|
|
|
71
|
+
if downloaded_bytes > content_length
|
|
72
|
+
raise Faraday::Error, 'File is bigger than informed, aborting download'
|
|
73
|
+
end
|
|
57
74
|
|
|
58
|
-
|
|
59
|
-
|
|
75
|
+
f.write chunk
|
|
76
|
+
|
|
77
|
+
progress_bar.progress = downloaded_bytes if progress
|
|
60
78
|
end
|
|
61
79
|
end
|
|
80
|
+
rescue Faraday::Error => e
|
|
81
|
+
::Jekyll.logger.warn 'Error:', e.message
|
|
62
82
|
end
|
|
63
83
|
end
|
|
64
84
|
|
|
85
|
+
progress_bar.stop if progress
|
|
65
86
|
File.exist? dest
|
|
66
87
|
end
|
|
88
|
+
|
|
89
|
+
# Local path following Sutty's conventions. Instead of producing
|
|
90
|
+
# a SecureRandom.base36 key, we use the first 28 characters of the
|
|
91
|
+
# digest to obtain constand names.
|
|
92
|
+
#
|
|
93
|
+
# It then hardlinks the original file.
|
|
94
|
+
#
|
|
95
|
+
# @return [String]
|
|
96
|
+
def public_path
|
|
97
|
+
@public_path ||= File.join('public', digest[..27], dest.split('/').last).tap do
|
|
98
|
+
next if File.exist? _1
|
|
99
|
+
|
|
100
|
+
# If the directory exists and it's not empty, but the file is
|
|
101
|
+
# not there, we either have the same file with different names
|
|
102
|
+
# or old thumbnails.
|
|
103
|
+
dir = File.dirname(_1)
|
|
104
|
+
if File.directory?(dir) && !Dir.empty?(dir)
|
|
105
|
+
Jekyll::Logger.warn 'Migration:',
|
|
106
|
+
"#{dir} is not empty, you probably have the same file with different names"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
FileUtils.mkdir_p(File.dirname(_1))
|
|
110
|
+
FileUtils.ln(dest, _1)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Download and produce a digest. Produce an error if the file
|
|
115
|
+
# can't be downloaded.
|
|
116
|
+
#
|
|
117
|
+
# @return [String]
|
|
118
|
+
def digest
|
|
119
|
+
@digest ||=
|
|
120
|
+
begin
|
|
121
|
+
require 'digest'
|
|
122
|
+
|
|
123
|
+
raise Faraday::Error unless download
|
|
124
|
+
|
|
125
|
+
Digest::SHA2.file(dest).hexdigest
|
|
126
|
+
end
|
|
127
|
+
end
|
|
67
128
|
end
|
|
68
129
|
end
|
|
69
130
|
end
|
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'rails-html-sanitizer'
|
|
3
4
|
require 'wordpress_formatting/wpautop'
|
|
5
|
+
require 'htmlbeautifier'
|
|
4
6
|
require 'jekyll/utils'
|
|
7
|
+
require 'nokogiri'
|
|
5
8
|
|
|
6
9
|
module SuttyMigration
|
|
7
10
|
class WordpressXml
|
|
8
11
|
# Represents a WordPress post
|
|
9
12
|
class Post
|
|
10
13
|
attr_reader :wordpress, :item
|
|
14
|
+
attr_writer :tags, :categories, :author, :meta, :date, :last_modified_at
|
|
11
15
|
|
|
16
|
+
SANITIZER = Rails::HTML::Sanitizer.safe_list_sanitizer.new
|
|
17
|
+
# This follows https://www.npmjs.com/package/@suttyweb/editor and
|
|
18
|
+
# also allows some known WP tags, you can add your own during
|
|
19
|
+
# plugin initilization.
|
|
20
|
+
#
|
|
21
|
+
# rubocop:disable Style/MutableConstant
|
|
22
|
+
TAGS = %w[a aside audio blockquote br caption code del em figcaption figure h1 h2 h3 h4 h5 h6 hr iframe img li mark ol p
|
|
23
|
+
pre small strong sub sup table tbody td tfoot th thead tr u ul video]
|
|
24
|
+
ATTRIBUTES = %w[alt colspan controls data-align data-id data-lang data-multimedia data-multimedia-inner
|
|
25
|
+
data-turbo href id name referrerpolicy rel reversed rowspan src start target title type]
|
|
26
|
+
# rubocop:enable Style/MutableConstant
|
|
12
27
|
SLASH = '/'
|
|
28
|
+
THUMBNAIL_PREFIX = /(-scaled)?(-\d+x\d+)?(\.....?)\z/
|
|
29
|
+
# TODO: Move to wordpress-formatting
|
|
30
|
+
WORDPRESS_ELEMENT_START = /\[(\w+ [^\]]+)\]/
|
|
31
|
+
WORDPRESS_ELEMENT_END = %r{\[(/\w+)\]}
|
|
13
32
|
|
|
14
33
|
# @param :wordpress [SuttyMigration::WordpressXml]
|
|
15
|
-
# @param :item [Nokogiri::XML::Element]
|
|
34
|
+
# @param :item [Nokogiri::XML::Element, Hash]
|
|
16
35
|
def initialize(wordpress:, item:)
|
|
17
36
|
@wordpress = wordpress
|
|
18
37
|
@item = item
|
|
@@ -35,13 +54,13 @@ module SuttyMigration
|
|
|
35
54
|
def permalink
|
|
36
55
|
@permalink ||=
|
|
37
56
|
begin
|
|
38
|
-
p = attribute_value('link')
|
|
57
|
+
p = attribute_value('link')&.delete(wordpress.url)
|
|
39
58
|
|
|
40
|
-
|
|
41
|
-
p << SLASH
|
|
42
|
-
end
|
|
59
|
+
unless p.nil?
|
|
60
|
+
p << SLASH if !p.end_with?(SLASH) && File.extname(p).empty?
|
|
43
61
|
|
|
44
|
-
|
|
62
|
+
p.squeeze(SLASH)
|
|
63
|
+
end
|
|
45
64
|
end
|
|
46
65
|
end
|
|
47
66
|
|
|
@@ -63,7 +82,9 @@ module SuttyMigration
|
|
|
63
82
|
#
|
|
64
83
|
# @return [String]
|
|
65
84
|
def slug
|
|
66
|
-
@slug ||= attribute_value('post_name').tr(SLASH, '-').squeeze('-').sub(/\A-+/, '').sub(
|
|
85
|
+
@slug ||= (attribute_value('post_name') || attribute_value('slug')).to_s.tr(SLASH, '-').squeeze('-').sub(/\A-+/, '').sub(
|
|
86
|
+
/-+\z/, ''
|
|
87
|
+
)
|
|
67
88
|
end
|
|
68
89
|
|
|
69
90
|
# Publication date.
|
|
@@ -89,13 +110,80 @@ module SuttyMigration
|
|
|
89
110
|
end
|
|
90
111
|
end
|
|
91
112
|
|
|
92
|
-
# Content as HTML,
|
|
113
|
+
# Content as an HTML string, converting Wordpress [tags] to <tags>
|
|
93
114
|
#
|
|
94
115
|
# @return [String]
|
|
95
116
|
def content
|
|
96
|
-
@content ||= WordpressFormatting::Wpautop.wpautop(attribute_value('encoded')).gsub(
|
|
97
|
-
|
|
98
|
-
|
|
117
|
+
@content ||= WordpressFormatting::Wpautop.wpautop((attribute_value('encoded') || attribute_value('content')).gsub(WORDPRESS_ELEMENT_START, '<\\1>').gsub(
|
|
118
|
+
WORDPRESS_ELEMENT_END, '<\\1>'
|
|
119
|
+
))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Remove extraneous code
|
|
123
|
+
#
|
|
124
|
+
# @return [String]
|
|
125
|
+
def sanitized_content
|
|
126
|
+
@sanitized_content ||= SANITIZER.sanitize(content.to_s, tags: TAGS, attributes: ATTRIBUTES)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Returns a fragment that can be queried using Nokogiri and
|
|
130
|
+
# transformed for final migration. Applies common transformations
|
|
131
|
+
# too.
|
|
132
|
+
#
|
|
133
|
+
# @return [Nokogiri::HTML]
|
|
134
|
+
def parsed_content
|
|
135
|
+
@parsed_content ||= Nokogiri::HTML.fragment(sanitized_content).tap do |html|
|
|
136
|
+
html.css('[src]').each do |element|
|
|
137
|
+
# Remove the thumbnail suffix, we only want the original files
|
|
138
|
+
element['src'] = element['src'].sub(THUMBNAIL_PREFIX, '\\3')
|
|
139
|
+
|
|
140
|
+
if element['src'].start_with? wordpress.url
|
|
141
|
+
element['src'] = wordpress.attachments[CGI.unescape element['src']].public_path
|
|
142
|
+
|
|
143
|
+
# Move the element into a <figure> like our editor does
|
|
144
|
+
unless element.parent && element.parent.name == '#document-fragment'
|
|
145
|
+
parent = element.parent
|
|
146
|
+
|
|
147
|
+
parent = parent.parent until parent.parent.nil? || parent.parent.name == '#document-fragment'
|
|
148
|
+
|
|
149
|
+
figure = parent.add_previous_sibling('<figure></figure>').first
|
|
150
|
+
figure.add_child element
|
|
151
|
+
# This was the [caption] WP element
|
|
152
|
+
figcaption = html.css('caption').first
|
|
153
|
+
|
|
154
|
+
if figcaption
|
|
155
|
+
figcaption.name = 'figcaption'
|
|
156
|
+
figure.add_child figcaption
|
|
157
|
+
element['alt'] = figcaption.text.strip
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Iframes are converted to an autolinked paragraph so
|
|
162
|
+
# jekyll-embed-urls can pick them up during build
|
|
163
|
+
elsif element.name == 'iframe'
|
|
164
|
+
element.parent.name = 'p'
|
|
165
|
+
element.name = 'a'
|
|
166
|
+
element['href'] = element['src']
|
|
167
|
+
if element['href'].include? 'youtube.com/embed/'
|
|
168
|
+
element['href'] = Addressable::URI.parse(element['href']).tap do |uri|
|
|
169
|
+
uri.query = URI.encode_www_form(URI.decode_www_form(uri.query.to_s).tap do |query|
|
|
170
|
+
query << ['v', uri.path.split('/').last]
|
|
171
|
+
end)
|
|
172
|
+
uri.path = '/watch'
|
|
173
|
+
end.to_s
|
|
174
|
+
end
|
|
175
|
+
element.delete 'src'
|
|
176
|
+
element.inner_html = element['href']
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Beautifies the parsed content.
|
|
183
|
+
#
|
|
184
|
+
# @return [String]
|
|
185
|
+
def beautiful_content
|
|
186
|
+
HtmlBeautifier.beautify(parsed_content.to_s.squeeze(' ').gsub(/>\s+</, '><'))
|
|
99
187
|
end
|
|
100
188
|
|
|
101
189
|
# Author attributes.
|
|
@@ -176,9 +264,16 @@ module SuttyMigration
|
|
|
176
264
|
|
|
177
265
|
# Get a value from the attribute
|
|
178
266
|
#
|
|
267
|
+
# @param key [String, Symbol]
|
|
179
268
|
# @return [String, nil]
|
|
180
269
|
def attribute_value(key)
|
|
181
|
-
item
|
|
270
|
+
(case item
|
|
271
|
+
when Hash
|
|
272
|
+
item[key.to_sym]
|
|
273
|
+
else
|
|
274
|
+
item.at_css(key.to_s)&.text
|
|
275
|
+
end
|
|
276
|
+
)&.unicode_normalize&.strip
|
|
182
277
|
end
|
|
183
278
|
end
|
|
184
279
|
end
|
|
@@ -117,18 +117,23 @@ module SuttyMigration
|
|
|
117
117
|
#
|
|
118
118
|
# @return [Hash]
|
|
119
119
|
def pages
|
|
120
|
-
@pages ||= items_find_by('post_type', 'page').
|
|
121
|
-
|
|
122
|
-
end
|
|
120
|
+
@pages ||= items_find_by('post_type', 'page').to_h do |item|
|
|
121
|
+
[attribute_value(page, 'post_id').to_i, Post.new(wordpress: self, item:)]
|
|
122
|
+
end
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
-
# Attachments, indexed by ID
|
|
125
|
+
# Attachments, indexed by URL and ID
|
|
126
126
|
#
|
|
127
127
|
# @return [Hash]
|
|
128
128
|
def attachments
|
|
129
|
-
@attachments ||= items_find_by('post_type', 'attachment').map do |
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
@attachments ||= items_find_by('post_type', 'attachment').map do |item|
|
|
130
|
+
attachment = Attachment.new(wordpress: self, item:)
|
|
131
|
+
|
|
132
|
+
[
|
|
133
|
+
[attribute_value(item, 'post_id').to_i, attachment],
|
|
134
|
+
[attribute_value(item, 'attachment_url'), attachment]
|
|
135
|
+
]
|
|
136
|
+
end.flatten(1).to_h
|
|
132
137
|
end
|
|
133
138
|
|
|
134
139
|
# Find items by attribute and value
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sutty-migration
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0rc0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- f
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-10-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: jekyll
|
|
@@ -38,6 +38,20 @@ dependencies:
|
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: '0.1'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: addressable
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
41
55
|
- !ruby/object:Gem::Dependency
|
|
42
56
|
name: fast_blank
|
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -150,6 +164,34 @@ dependencies:
|
|
|
150
164
|
- - "~>"
|
|
151
165
|
- !ruby/object:Gem::Version
|
|
152
166
|
version: 1.3.0
|
|
167
|
+
- !ruby/object:Gem::Dependency
|
|
168
|
+
name: htmlbeautifier
|
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
|
170
|
+
requirements:
|
|
171
|
+
- - ">="
|
|
172
|
+
- !ruby/object:Gem::Version
|
|
173
|
+
version: '0'
|
|
174
|
+
type: :runtime
|
|
175
|
+
prerelease: false
|
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
177
|
+
requirements:
|
|
178
|
+
- - ">="
|
|
179
|
+
- !ruby/object:Gem::Version
|
|
180
|
+
version: '0'
|
|
181
|
+
- !ruby/object:Gem::Dependency
|
|
182
|
+
name: rails-html-sanitizer
|
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
|
184
|
+
requirements:
|
|
185
|
+
- - ">="
|
|
186
|
+
- !ruby/object:Gem::Version
|
|
187
|
+
version: '0'
|
|
188
|
+
type: :runtime
|
|
189
|
+
prerelease: false
|
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
191
|
+
requirements:
|
|
192
|
+
- - ">="
|
|
193
|
+
- !ruby/object:Gem::Version
|
|
194
|
+
version: '0'
|
|
153
195
|
- !ruby/object:Gem::Dependency
|
|
154
196
|
name: pry
|
|
155
197
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -164,7 +206,21 @@ dependencies:
|
|
|
164
206
|
- - ">="
|
|
165
207
|
- !ruby/object:Gem::Version
|
|
166
208
|
version: '0'
|
|
167
|
-
|
|
209
|
+
- !ruby/object:Gem::Dependency
|
|
210
|
+
name: rubocop
|
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
|
212
|
+
requirements:
|
|
213
|
+
- - ">="
|
|
214
|
+
- !ruby/object:Gem::Version
|
|
215
|
+
version: '0'
|
|
216
|
+
type: :development
|
|
217
|
+
prerelease: false
|
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
219
|
+
requirements:
|
|
220
|
+
- - ">="
|
|
221
|
+
- !ruby/object:Gem::Version
|
|
222
|
+
version: '0'
|
|
223
|
+
description: Takes datafiles, Wordpress backups and converts them into Jekyll posts
|
|
168
224
|
email:
|
|
169
225
|
- f@sutty.nl
|
|
170
226
|
executables: []
|
|
@@ -175,6 +231,8 @@ extra_rdoc_files:
|
|
|
175
231
|
files:
|
|
176
232
|
- LICENSE.txt
|
|
177
233
|
- README.md
|
|
234
|
+
- lib/jekyll-wordpress-sql-migration.rb
|
|
235
|
+
- lib/jekyll-wordpress-xml-migration.rb
|
|
178
236
|
- lib/sutty-migration.rb
|
|
179
237
|
- lib/sutty_migration/core_extensions.rb
|
|
180
238
|
- lib/sutty_migration/data.rb
|
|
@@ -183,19 +241,18 @@ files:
|
|
|
183
241
|
- lib/sutty_migration/wordpress_xml.rb
|
|
184
242
|
- lib/sutty_migration/wordpress_xml/attachment.rb
|
|
185
243
|
- lib/sutty_migration/wordpress_xml/post.rb
|
|
186
|
-
homepage: https://
|
|
244
|
+
homepage: https://git.sutty.abyaya.la/jekyll/sutty-migration
|
|
187
245
|
licenses:
|
|
188
246
|
- GPL-3.0
|
|
189
247
|
metadata:
|
|
190
|
-
bug_tracker_uri: https://
|
|
191
|
-
homepage_uri: https://
|
|
192
|
-
source_code_uri: https://
|
|
193
|
-
changelog_uri: https://0xacab.org/sutty/jekyll/sutty-migration/-/blob/master/CHANGELOG.md
|
|
248
|
+
bug_tracker_uri: https://git.sutty.abyaya.la/jekyll/sutty-migration/issues
|
|
249
|
+
homepage_uri: https://git.sutty.abyaya.la/jekyll/sutty-migration
|
|
250
|
+
source_code_uri: https://git.sutty.abyaya.la/jekyll/sutty-migration
|
|
194
251
|
documentation_uri: https://rubydoc.info/gems/sutty-migration
|
|
195
252
|
post_install_message:
|
|
196
253
|
rdoc_options:
|
|
197
254
|
- "--title"
|
|
198
|
-
- sutty-migration -
|
|
255
|
+
- sutty-migration - CSV, Wordpress SQL and Wordpress XML migration
|
|
199
256
|
- "--main"
|
|
200
257
|
- README.md
|
|
201
258
|
- "--line-numbers"
|
|
@@ -207,15 +264,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
207
264
|
requirements:
|
|
208
265
|
- - ">="
|
|
209
266
|
- !ruby/object:Gem::Version
|
|
210
|
-
version:
|
|
267
|
+
version: '3.1'
|
|
211
268
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
212
269
|
requirements:
|
|
213
|
-
- - "
|
|
270
|
+
- - ">"
|
|
214
271
|
- !ruby/object:Gem::Version
|
|
215
|
-
version:
|
|
272
|
+
version: 1.3.1
|
|
216
273
|
requirements: []
|
|
217
274
|
rubygems_version: 3.3.27
|
|
218
275
|
signing_key:
|
|
219
276
|
specification_version: 4
|
|
220
|
-
summary:
|
|
277
|
+
summary: CSV, Wordpress SQL and Wordpress XML migration
|
|
221
278
|
test_files: []
|