compostr 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|