compostr 0.1.0
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 +7 -0
- data/.gitignore +9 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE +674 -0
- data/README.md +94 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/compostr.gemspec +32 -0
- data/lib/compostr.rb +60 -0
- data/lib/compostr/custom_field_value.rb +36 -0
- data/lib/compostr/custom_post_type.rb +386 -0
- data/lib/compostr/entity_cache.rb +100 -0
- data/lib/compostr/image_upload.rb +31 -0
- data/lib/compostr/image_uploader.rb +37 -0
- data/lib/compostr/logging.rb +25 -0
- data/lib/compostr/media_library_cache.rb +51 -0
- data/lib/compostr/syncer.rb +88 -0
- data/lib/compostr/version.rb +3 -0
- data/lib/compostr/wp_string.rb +14 -0
- data/vcr_cassettes/syncer_push_dungeonlord.yml +62 -0
- data/vcr_cassettes/wp_failing_post_deletion.yml +61 -0
- data/vcr_cassettes/wp_successful_post_deletion.yml +62 -0
- metadata +166 -0
@@ -0,0 +1,100 @@
|
|
1
|
+
module Compostr
|
2
|
+
class EntityCache
|
3
|
+
attr_accessor :cpt_class, :name_id_map, :uuid_id_map
|
4
|
+
attr_accessor :full_data
|
5
|
+
|
6
|
+
# cpt_class has to be descendant of Compostr::CustomPostType
|
7
|
+
def initialize cpt_class
|
8
|
+
@cpt_class = cpt_class
|
9
|
+
@name_id_map = nil
|
10
|
+
@uuid_id_map = nil
|
11
|
+
if !(@cpt_class < Compostr::CustomPostType)
|
12
|
+
raise "Unsupported Entity for EntityCache: #{@cpt_class}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Pretty nasty stuff. Stores all posts of cpt in memory (alas,
|
17
|
+
# only once) and looks through them until one with uuid found.
|
18
|
+
def in_mem_lookup uuid
|
19
|
+
# TODO index (hash) on (uu)id, access index only
|
20
|
+
if full_data.length == 10_000
|
21
|
+
# warn heavily
|
22
|
+
elsif full_data.length > 1000
|
23
|
+
# warn softly
|
24
|
+
end
|
25
|
+
uuid_selector = lambda do |x|
|
26
|
+
x["custom_fields"].find do |f|
|
27
|
+
f["key"] == "uuid" && f["value"] == uuid
|
28
|
+
end
|
29
|
+
end
|
30
|
+
full_data.find &uuid_selector
|
31
|
+
#@full_data.find &WPEvent::Lambdas.with_cf_uuid(uuid)
|
32
|
+
end
|
33
|
+
|
34
|
+
def id_of_name name
|
35
|
+
return [] if name.nil? || name.empty?
|
36
|
+
name_id_map[name]
|
37
|
+
end
|
38
|
+
|
39
|
+
def id_of_names names
|
40
|
+
return [] if names.nil? || names.empty?
|
41
|
+
names.map{|name| name_id_map[name]}
|
42
|
+
end
|
43
|
+
|
44
|
+
def id_of_uuid uuid
|
45
|
+
return [] if uuid.nil? || uuid.empty?
|
46
|
+
uuid_id_map[uuid]
|
47
|
+
end
|
48
|
+
|
49
|
+
def id_of_uuids uuids
|
50
|
+
return [] if uuids.nil? || uuids.empty?
|
51
|
+
uuids.map{|uuid| id_of_uuid uuid}
|
52
|
+
end
|
53
|
+
|
54
|
+
# init and return @name_id_map
|
55
|
+
def name_id_map
|
56
|
+
@name_id_map ||= full_data.map {|p| [p["post_title"], p["post_id"]]}.to_h
|
57
|
+
end
|
58
|
+
|
59
|
+
def uuid_id_map
|
60
|
+
@uuid_id_map ||= uuid_pid_map
|
61
|
+
end
|
62
|
+
|
63
|
+
# Burn-in cache
|
64
|
+
def full_data
|
65
|
+
@full_data ||= get_all_posts
|
66
|
+
end
|
67
|
+
|
68
|
+
# Select entities for which given selector returns true
|
69
|
+
# example: selector = lambda {|e| e.to_s == 'keepme'}
|
70
|
+
def select_by selector
|
71
|
+
full_data.select &selector
|
72
|
+
end
|
73
|
+
|
74
|
+
def by_post_id id
|
75
|
+
if @full_data.nil?
|
76
|
+
Compostr::wp.getPost blog_id: 0,
|
77
|
+
post_id: id
|
78
|
+
else
|
79
|
+
@full_data.find do |p|
|
80
|
+
p["post_id"] == id
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def get_all_posts
|
88
|
+
Compostr::wp.getPosts blog_id: 0,
|
89
|
+
filter: { post_type: @cpt_class.post_type, number: 100_000 }
|
90
|
+
end
|
91
|
+
|
92
|
+
def uuid_pid_map
|
93
|
+
full_data.map do |post|
|
94
|
+
# TODO lambda
|
95
|
+
uuid = post["custom_fields"].find {|f| f["key"] == "uuid"}&.fetch("value", nil)
|
96
|
+
[uuid, post["post_id"]]
|
97
|
+
end.to_h
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'mime-types'
|
2
|
+
|
3
|
+
module Compostr
|
4
|
+
class ImageUpload
|
5
|
+
attr_accessor :file_path, :post_id
|
6
|
+
|
7
|
+
def initialize file_path, post_id=nil
|
8
|
+
# TODO decide on getMediaLibrary to find an already uploaded image
|
9
|
+
@file_path = file_path
|
10
|
+
@post_id = post_id
|
11
|
+
end
|
12
|
+
|
13
|
+
# Push data to Wordpress instance, return attachment_id
|
14
|
+
def do_upload!
|
15
|
+
data = create_data
|
16
|
+
response = Compostr::wp.uploadFile(data: data)
|
17
|
+
response["attachment_id"]
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def create_data
|
23
|
+
{
|
24
|
+
name: File.basename(@file_path),
|
25
|
+
type: MIME::Types.type_for(file_path).first.to_s,
|
26
|
+
post_id: @post_id || '',
|
27
|
+
bits: XMLRPC::Base64.new(IO.read file_path)
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'mime-types'
|
2
|
+
|
3
|
+
module Compostr
|
4
|
+
class ImageUploader
|
5
|
+
attr_accessor :image_store
|
6
|
+
attr_accessor :media_cache
|
7
|
+
|
8
|
+
def initialize image_store, media_cache
|
9
|
+
if !image_store
|
10
|
+
Compostr.logger.warn "ImageUploader will not uploading anything (no image store specified)."
|
11
|
+
end
|
12
|
+
@image_store = image_store
|
13
|
+
@media_cache = media_cache
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns attachment id of an image already uploaded
|
17
|
+
# or attachment_id of image after fresh upload
|
18
|
+
# (or nil if path empty)
|
19
|
+
def process rel_path, wp_event=nil
|
20
|
+
return nil if rel_path.to_s.strip.empty?
|
21
|
+
return nil if @image_store.nil?
|
22
|
+
|
23
|
+
# Do we need URI encoding here?
|
24
|
+
if attachment_id = @media_cache.id_of_name(rel_path)
|
25
|
+
Compostr.logger.debug "Image already uploaded."
|
26
|
+
else
|
27
|
+
path = File.join(@image_store, rel_path)
|
28
|
+
Compostr.logger.info "Uploading file #{path}"
|
29
|
+
upload = Compostr::ImageUpload.new(path)
|
30
|
+
attachment_id = upload.do_upload!
|
31
|
+
Compostr.logger.debug "Uploaded image id: #{attachment_id}"
|
32
|
+
end
|
33
|
+
|
34
|
+
return attachment_id
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Compostr
|
4
|
+
# Module to extend to get easy access to standard log functions.
|
5
|
+
# These are debug, info, warn, error and fatal.
|
6
|
+
# All log functions use the Compostr.logger (which can be customized).
|
7
|
+
# A typical client will just `extend Compostr::Logging` .
|
8
|
+
module Logging
|
9
|
+
def debug msg
|
10
|
+
Compostr.logger.debug msg
|
11
|
+
end
|
12
|
+
def info msg
|
13
|
+
Compostr.logger.info msg
|
14
|
+
end
|
15
|
+
def warn msg
|
16
|
+
Compostr.logger.warn msg
|
17
|
+
end
|
18
|
+
def error msg
|
19
|
+
Compostr.logger.error msg
|
20
|
+
end
|
21
|
+
def fatal msg
|
22
|
+
Compostr.logger.fatal msg
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Compostr
|
2
|
+
# Cache name->id for media items
|
3
|
+
# pretty wet copy of entitycache. Unclear how to DRY this up.
|
4
|
+
# Wordpress apparently lowercases file endings on upload, so this fact
|
5
|
+
# is respected in the lookup (only file ending is modified).
|
6
|
+
class MediaLibraryCache
|
7
|
+
attr_accessor :name_id_map
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@name_id_map = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
# return id of given name, initializing the cache
|
14
|
+
# if necessary
|
15
|
+
def id_of_name name
|
16
|
+
return [] if name.nil? || name.empty?
|
17
|
+
# Downcase file ending
|
18
|
+
name_for_wp = File.basename(name, '.*') + File.extname(name).downcase
|
19
|
+
name_id_map[name_for_wp]
|
20
|
+
end
|
21
|
+
|
22
|
+
# return array of ids to given names, initializing the cache
|
23
|
+
# if necessary
|
24
|
+
def id_of_names names
|
25
|
+
return [] if names.nil? || names.empty?
|
26
|
+
names.map{|name| id_of_name name }
|
27
|
+
end
|
28
|
+
|
29
|
+
# init and return @name_id_map
|
30
|
+
def name_id_map
|
31
|
+
if @name_id_map.nil?
|
32
|
+
@name_id_map = create_name_id_map
|
33
|
+
end
|
34
|
+
@name_id_map || {}
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
def create_name_id_map
|
39
|
+
items = Compostr::wp.getMediaLibrary(blog_id: 0)
|
40
|
+
|
41
|
+
# warn if size is interesting.
|
42
|
+
|
43
|
+
items.map do |i|
|
44
|
+
uri = URI.parse URI.encode(i['link'])
|
45
|
+
filename = File.basename uri.path
|
46
|
+
|
47
|
+
[filename, i['attachment_id']]
|
48
|
+
end.to_h
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Compostr
|
2
|
+
# Magically syncs custom posts between json, intermediate representation
|
3
|
+
# or other forms with data in wordpress. Scary API award.
|
4
|
+
class Syncer
|
5
|
+
include Compostr::Logging
|
6
|
+
|
7
|
+
attr_accessor :image_uploader
|
8
|
+
attr_accessor :synced_uuids
|
9
|
+
attr_accessor :updated_uuids
|
10
|
+
|
11
|
+
def initialize image_uploader
|
12
|
+
@image_uploader = image_uploader
|
13
|
+
@synced_uuids = []
|
14
|
+
@updated_uuids = []
|
15
|
+
end
|
16
|
+
|
17
|
+
# Updates or creates Custom Post Types Posts.
|
18
|
+
#
|
19
|
+
# The post will be identified by uuid (or not).
|
20
|
+
#
|
21
|
+
# new_post_content
|
22
|
+
# The data that **should** be in wordpress (without knowing
|
23
|
+
# of wordpress post or custom field ids). Usually descendant of CustomPostType.
|
24
|
+
#
|
25
|
+
# old_post
|
26
|
+
# The data currently available in wordpress (including
|
27
|
+
# wordpress post id, custom field ids).
|
28
|
+
def merge_push new_post, old_post
|
29
|
+
if old_post && old_post.in_wordpress?
|
30
|
+
info "#{new_post.class.name} with UUID #{new_post.uuid} found, updating"
|
31
|
+
debug old_post.fields.inspect
|
32
|
+
|
33
|
+
new_post.different_from? old_post
|
34
|
+
|
35
|
+
new_post.post_id = old_post.post_id
|
36
|
+
new_post.integrate_field_ids old_post
|
37
|
+
|
38
|
+
# TODO unclear how to deal with images
|
39
|
+
#attachment_id = @image_uploader.process json['image_url']
|
40
|
+
#new_entity.featured_image_id = attachment_id
|
41
|
+
|
42
|
+
#TODO one (cat) is missing when updating single via bundle json
|
43
|
+
|
44
|
+
content = new_post.to_content_hash
|
45
|
+
adjust_content content
|
46
|
+
# TODO and image ...
|
47
|
+
|
48
|
+
debug "Upload Post ##{new_post.post_id} with wp-content: #{content}"
|
49
|
+
|
50
|
+
post_id = Compostr::wp.editPost(blog_id: 0,
|
51
|
+
post_id: new_post.post_id,
|
52
|
+
content: content)
|
53
|
+
if post_id
|
54
|
+
info "#{new_post.class} ##{new_post.post_id} updated"
|
55
|
+
else
|
56
|
+
info "#{new_post.class} ##{new_post.post_id} not updated!"
|
57
|
+
end
|
58
|
+
else
|
59
|
+
# Easy, create new one
|
60
|
+
info "#{new_post.class.name} with UUID #{new_post.uuid} not found, creating"
|
61
|
+
content = new_post.to_content_hash
|
62
|
+
adjust_content content
|
63
|
+
|
64
|
+
# Ouch ....
|
65
|
+
#attachment_id = @image_uploader.process json['image_url']
|
66
|
+
#custom_post.featured_image_id = attachment_id
|
67
|
+
|
68
|
+
debug "Create Post with wp-content: #{content}"
|
69
|
+
|
70
|
+
new_post_id = Compostr::wp.newPost(blog_id: 0,
|
71
|
+
content: content)
|
72
|
+
if new_post_id
|
73
|
+
info "#{new_post.class} with WP ID #{new_post_id} created"
|
74
|
+
else
|
75
|
+
info "#{new_post.class} not created!"
|
76
|
+
end
|
77
|
+
new_post.post_id = new_post_id
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Add language term and post author data to WP content hash.
|
82
|
+
def adjust_content content
|
83
|
+
content[:terms_names] = { 'language' => [ Compostr::config.language_term || 'Deutsch' ] }
|
84
|
+
content[:post_author] = Compostr::config.author_id || 1
|
85
|
+
# publish? Date ... ?
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: post
|
5
|
+
uri: http://wordpress.mydomain/xmlrpc.php
|
6
|
+
body:
|
7
|
+
encoding: UTF-8
|
8
|
+
string: '<?xml version="1.0" ?><methodCall><methodName>wp.newPost</methodName><params><param><value><array><data><value><i4>0</i4></value><value><string>admin</string></value><value><string>buzzword</string></value><value><struct><member><name>post_type</name><value><string>boardgame</string></value></member><member><name>post_status</name><value><string>publish</string></value></member><member><name>post_data</name><value><dateTime.iso8601>20170518T11:42:23</dateTime.iso8601></value></member><member><name>post_title</name><value><string>Dungeonlord</string></value></member><member><name>post_content</name><value><string></string></value></member><member><name>custom_fields</name><value><array><data/></array></value></member><member><name>terms_names</name><value><struct><member><name>language</name><value><array><data><value><string>Deutsch</string></value></data></array></value></member></struct></value></member><member><name>post_author</name><value><i4>1</i4></value></member></struct></value></data></array></value></param></params></methodCall>
|
9
|
+
|
10
|
+
'
|
11
|
+
headers:
|
12
|
+
User-Agent:
|
13
|
+
- XMLRPC::Client (Ruby 2.3.1)
|
14
|
+
Content-Type:
|
15
|
+
- text/xml; charset=utf-8
|
16
|
+
Content-Length:
|
17
|
+
- '1056'
|
18
|
+
Connection:
|
19
|
+
- keep-alive
|
20
|
+
Accept-Encoding:
|
21
|
+
- identity
|
22
|
+
Accept:
|
23
|
+
- "*/*"
|
24
|
+
response:
|
25
|
+
status:
|
26
|
+
code: 200
|
27
|
+
message: OK
|
28
|
+
headers:
|
29
|
+
Date:
|
30
|
+
- Thu, 18 May 2017 09:31:43 GMT
|
31
|
+
Server:
|
32
|
+
- Apache/2.2.14 (Ubuntu)
|
33
|
+
X-Powered-By:
|
34
|
+
- PHP/5.3.2-1ubuntu4.30
|
35
|
+
Connection:
|
36
|
+
- close
|
37
|
+
X-Content-Security-Policy:
|
38
|
+
- allow *
|
39
|
+
Content-Security-Policy:
|
40
|
+
- frame-ancestors 'self'
|
41
|
+
Vary:
|
42
|
+
- Accept-Encoding
|
43
|
+
Content-Length:
|
44
|
+
- '179'
|
45
|
+
Content-Type:
|
46
|
+
- text/xml; charset=UTF-8
|
47
|
+
body:
|
48
|
+
encoding: UTF-8
|
49
|
+
string: |
|
50
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
51
|
+
<methodResponse>
|
52
|
+
<params>
|
53
|
+
<param>
|
54
|
+
<value>
|
55
|
+
<string>2766</string>
|
56
|
+
</value>
|
57
|
+
</param>
|
58
|
+
</params>
|
59
|
+
</methodResponse>
|
60
|
+
http_version:
|
61
|
+
recorded_at: Thu, 18 May 2017 09:42:25 GMT
|
62
|
+
recorded_with: VCR 3.0.3
|
@@ -0,0 +1,61 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: post
|
5
|
+
uri: http://wordpress.mydomain/xmlrpc.php
|
6
|
+
body:
|
7
|
+
encoding: UTF-8
|
8
|
+
string: '<?xml version="1.0" ?><methodCall><methodName>wp.deletePost</methodName><params><param><value><array><data><value><i4>0</i4></value><value><string>admin</string></value><value><string>buzzword</string></value><value><string>100000</string></value></data></array></value></param></params></methodCall>
|
9
|
+
|
10
|
+
'
|
11
|
+
headers:
|
12
|
+
User-Agent:
|
13
|
+
- XMLRPC::Client (Ruby 2.3.1)
|
14
|
+
Content-Type:
|
15
|
+
- text/xml; charset=utf-8
|
16
|
+
Content-Length:
|
17
|
+
- '298'
|
18
|
+
Connection:
|
19
|
+
- keep-alive
|
20
|
+
Accept-Encoding:
|
21
|
+
- identity
|
22
|
+
Accept:
|
23
|
+
- "*/*"
|
24
|
+
response:
|
25
|
+
status:
|
26
|
+
code: 200
|
27
|
+
message: OK
|
28
|
+
headers:
|
29
|
+
Date:
|
30
|
+
- Mon, 22 May 2017 14:04:20 GMT
|
31
|
+
Server:
|
32
|
+
- Apache/2.2.14 (Ubuntu)
|
33
|
+
X-Powered-By:
|
34
|
+
- PHP/5.3.2-1ubuntu4.30
|
35
|
+
Connection:
|
36
|
+
- close
|
37
|
+
X-Content-Security-Policy:
|
38
|
+
- allow *
|
39
|
+
Content-Security-Policy:
|
40
|
+
- frame-ancestors 'self'
|
41
|
+
Vary:
|
42
|
+
- Accept-Encoding
|
43
|
+
Content-Length:
|
44
|
+
- '395'
|
45
|
+
Content-Type:
|
46
|
+
- text/xml; charset=UTF-8
|
47
|
+
body:
|
48
|
+
encoding: ASCII-8BIT
|
49
|
+
string: !binary |-
|
50
|
+
PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPG1ldGhv
|
51
|
+
ZFJlc3BvbnNlPgogIDxmYXVsdD4KICAgIDx2YWx1ZT4KICAgICAgPHN0cnVj
|
52
|
+
dD4KICAgICAgICA8bWVtYmVyPgogICAgICAgICAgPG5hbWU+ZmF1bHRDb2Rl
|
53
|
+
PC9uYW1lPgogICAgICAgICAgPHZhbHVlPjxpbnQ+NDA0PC9pbnQ+PC92YWx1
|
54
|
+
ZT4KICAgICAgICA8L21lbWJlcj4KICAgICAgICA8bWVtYmVyPgogICAgICAg
|
55
|
+
ICAgPG5hbWU+ZmF1bHRTdHJpbmc8L25hbWU+CiAgICAgICAgICA8dmFsdWU+
|
56
|
+
PHN0cmluZz5VbmfDvGx0aWdlIEJlaXRyYWdzLUlELjwvc3RyaW5nPjwvdmFs
|
57
|
+
dWU+CiAgICAgICAgPC9tZW1iZXI+CiAgICAgIDwvc3RydWN0PgogICAgPC92
|
58
|
+
YWx1ZT4KICA8L2ZhdWx0Pgo8L21ldGhvZFJlc3BvbnNlPgo=
|
59
|
+
http_version:
|
60
|
+
recorded_at: Mon, 22 May 2017 14:15:31 GMT
|
61
|
+
recorded_with: VCR 3.0.3
|