##// END OF EJS Templates
Fixed syncing posts with internal attachments
neko259 -
r1707:0b70a151 default
parent child Browse files
Show More
@@ -1,323 +1,323 b''
1 import xml.etree.ElementTree as et
1 import xml.etree.ElementTree as et
2 import logging
2 import logging
3
3
4 from boards.abstracts.exceptions import SyncException
4 from boards.abstracts.exceptions import SyncException
5 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
5 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
6 from boards.models.attachment.downloaders import download
6 from boards.models.attachment.downloaders import download
7 from boards.utils import get_file_mimetype, get_file_hash
7 from boards.utils import get_file_mimetype, get_file_hash
8 from django.db import transaction
8 from django.db import transaction
9
9
10 EXCEPTION_NODE = 'Sync node returned an error: {}.'
10 EXCEPTION_NODE = 'Sync node returned an error: {}.'
11 EXCEPTION_OP = 'Load the OP first.'
11 EXCEPTION_OP = 'Load the OP first.'
12 EXCEPTION_DOWNLOAD = 'File was not downloaded.'
12 EXCEPTION_DOWNLOAD = 'File was not downloaded.'
13 EXCEPTION_HASH = 'File hash does not match attachment hash.'
13 EXCEPTION_HASH = 'File hash does not match attachment hash.'
14 EXCEPTION_SIGNATURE = 'Invalid model signature for {}.'
14 EXCEPTION_SIGNATURE = 'Invalid model signature for {}.'
15 EXCEPTION_AUTHOR_SIGNATURE = 'Model {} has no author signature.'
15 EXCEPTION_AUTHOR_SIGNATURE = 'Model {} has no author signature.'
16 ENCODING_UNICODE = 'unicode'
16 ENCODING_UNICODE = 'unicode'
17
17
18 TAG_MODEL = 'model'
18 TAG_MODEL = 'model'
19 TAG_REQUEST = 'request'
19 TAG_REQUEST = 'request'
20 TAG_RESPONSE = 'response'
20 TAG_RESPONSE = 'response'
21 TAG_ID = 'id'
21 TAG_ID = 'id'
22 TAG_STATUS = 'status'
22 TAG_STATUS = 'status'
23 TAG_MODELS = 'models'
23 TAG_MODELS = 'models'
24 TAG_TITLE = 'title'
24 TAG_TITLE = 'title'
25 TAG_TEXT = 'text'
25 TAG_TEXT = 'text'
26 TAG_THREAD = 'thread'
26 TAG_THREAD = 'thread'
27 TAG_PUB_TIME = 'pub-time'
27 TAG_PUB_TIME = 'pub-time'
28 TAG_SIGNATURES = 'signatures'
28 TAG_SIGNATURES = 'signatures'
29 TAG_SIGNATURE = 'signature'
29 TAG_SIGNATURE = 'signature'
30 TAG_CONTENT = 'content'
30 TAG_CONTENT = 'content'
31 TAG_ATTACHMENTS = 'attachments'
31 TAG_ATTACHMENTS = 'attachments'
32 TAG_ATTACHMENT = 'attachment'
32 TAG_ATTACHMENT = 'attachment'
33 TAG_TAGS = 'tags'
33 TAG_TAGS = 'tags'
34 TAG_TAG = 'tag'
34 TAG_TAG = 'tag'
35 TAG_ATTACHMENT_REFS = 'attachment-refs'
35 TAG_ATTACHMENT_REFS = 'attachment-refs'
36 TAG_ATTACHMENT_REF = 'attachment-ref'
36 TAG_ATTACHMENT_REF = 'attachment-ref'
37 TAG_TRIPCODE = 'tripcode'
37 TAG_TRIPCODE = 'tripcode'
38 TAG_VERSION = 'version'
38 TAG_VERSION = 'version'
39
39
40 TYPE_GET = 'get'
40 TYPE_GET = 'get'
41
41
42 ATTR_VERSION = 'version'
42 ATTR_VERSION = 'version'
43 ATTR_TYPE = 'type'
43 ATTR_TYPE = 'type'
44 ATTR_NAME = 'name'
44 ATTR_NAME = 'name'
45 ATTR_VALUE = 'value'
45 ATTR_VALUE = 'value'
46 ATTR_MIMETYPE = 'mimetype'
46 ATTR_MIMETYPE = 'mimetype'
47 ATTR_KEY = 'key'
47 ATTR_KEY = 'key'
48 ATTR_REF = 'ref'
48 ATTR_REF = 'ref'
49 ATTR_URL = 'url'
49 ATTR_URL = 'url'
50 ATTR_ID_TYPE = 'id-type'
50 ATTR_ID_TYPE = 'id-type'
51
51
52 ID_TYPE_MD5 = 'md5'
52 ID_TYPE_MD5 = 'md5'
53 ID_TYPE_URL = 'url'
53 ID_TYPE_URL = 'url'
54
54
55 STATUS_SUCCESS = 'success'
55 STATUS_SUCCESS = 'success'
56
56
57
57
58 logger = logging.getLogger('boards.sync')
58 logger = logging.getLogger('boards.sync')
59
59
60
60
61 class SyncManager:
61 class SyncManager:
62 @staticmethod
62 @staticmethod
63 def generate_response_get(model_list: list):
63 def generate_response_get(model_list: list):
64 response = et.Element(TAG_RESPONSE)
64 response = et.Element(TAG_RESPONSE)
65
65
66 status = et.SubElement(response, TAG_STATUS)
66 status = et.SubElement(response, TAG_STATUS)
67 status.text = STATUS_SUCCESS
67 status.text = STATUS_SUCCESS
68
68
69 models = et.SubElement(response, TAG_MODELS)
69 models = et.SubElement(response, TAG_MODELS)
70
70
71 for post in model_list:
71 for post in model_list:
72 model = et.SubElement(models, TAG_MODEL)
72 model = et.SubElement(models, TAG_MODEL)
73 model.set(ATTR_NAME, 'post')
73 model.set(ATTR_NAME, 'post')
74
74
75 global_id = post.global_id
75 global_id = post.global_id
76
76
77 attachments = post.attachments.all()
77 attachments = post.attachments.all()
78 if global_id.content:
78 if global_id.content:
79 model.append(et.fromstring(global_id.content))
79 model.append(et.fromstring(global_id.content))
80 if len(attachments) > 0:
80 if len(attachments) > 0:
81 internal_attachments = False
81 internal_attachments = False
82 for attachment in attachments:
82 for attachment in attachments:
83 if attachment.is_internal():
83 if attachment.is_internal():
84 internal_attachments = True
84 internal_attachments = True
85 break
85 break
86
86
87 if internal_attachments:
87 if internal_attachments:
88 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
88 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
89 for file in attachments:
89 for file in attachments:
90 SyncManager._attachment_to_xml(
90 SyncManager._attachment_to_xml(
91 None, attachment_refs, file)
91 None, attachment_refs, file)
92 else:
92 else:
93 content_tag = et.SubElement(model, TAG_CONTENT)
93 content_tag = et.SubElement(model, TAG_CONTENT)
94
94
95 tag_id = et.SubElement(content_tag, TAG_ID)
95 tag_id = et.SubElement(content_tag, TAG_ID)
96 global_id.to_xml_element(tag_id)
96 global_id.to_xml_element(tag_id)
97
97
98 title = et.SubElement(content_tag, TAG_TITLE)
98 title = et.SubElement(content_tag, TAG_TITLE)
99 title.text = post.title
99 title.text = post.title
100
100
101 text = et.SubElement(content_tag, TAG_TEXT)
101 text = et.SubElement(content_tag, TAG_TEXT)
102 text.text = post.get_sync_text()
102 text.text = post.get_sync_text()
103
103
104 thread = post.get_thread()
104 thread = post.get_thread()
105 if post.is_opening():
105 if post.is_opening():
106 tag_tags = et.SubElement(content_tag, TAG_TAGS)
106 tag_tags = et.SubElement(content_tag, TAG_TAGS)
107 for tag in thread.get_tags():
107 for tag in thread.get_tags():
108 tag_tag = et.SubElement(tag_tags, TAG_TAG)
108 tag_tag = et.SubElement(tag_tags, TAG_TAG)
109 tag_tag.text = tag.name
109 tag_tag.text = tag.name
110 else:
110 else:
111 tag_thread = et.SubElement(content_tag, TAG_THREAD)
111 tag_thread = et.SubElement(content_tag, TAG_THREAD)
112 thread_id = et.SubElement(tag_thread, TAG_ID)
112 thread_id = et.SubElement(tag_thread, TAG_ID)
113 thread.get_opening_post().global_id.to_xml_element(thread_id)
113 thread.get_opening_post().global_id.to_xml_element(thread_id)
114
114
115 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
115 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
116 pub_time.text = str(post.get_pub_time_str())
116 pub_time.text = str(post.get_pub_time_str())
117
117
118 if post.tripcode:
118 if post.tripcode:
119 tripcode = et.SubElement(content_tag, TAG_TRIPCODE)
119 tripcode = et.SubElement(content_tag, TAG_TRIPCODE)
120 tripcode.text = post.tripcode
120 tripcode.text = post.tripcode
121
121
122 if len(attachments) > 0:
122 if len(attachments) > 0:
123 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
123 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
124
124
125 internal_attachments = False
125 internal_attachments = False
126 for attachment in attachments:
126 for attachment in attachments:
127 if attachment.is_internal():
127 if attachment.is_internal():
128 internal_attachments = True
128 internal_attachments = True
129 break
129 break
130
130
131 if internal_attachments:
131 if internal_attachments:
132 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
132 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
133 else:
133 else:
134 attachment_refs = None
134 attachment_refs = None
135
135
136 for file in attachments:
136 for file in attachments:
137 SyncManager._attachment_to_xml(
137 SyncManager._attachment_to_xml(
138 attachments_tag, attachment_refs, file)
138 attachments_tag, attachment_refs, file)
139 version_tag = et.SubElement(content_tag, TAG_VERSION)
139 version_tag = et.SubElement(content_tag, TAG_VERSION)
140 version_tag.text = str(post.version)
140 version_tag.text = str(post.version)
141
141
142 global_id.content = et.tostring(content_tag, ENCODING_UNICODE)
142 global_id.content = et.tostring(content_tag, ENCODING_UNICODE)
143 global_id.save()
143 global_id.save()
144
144
145 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
145 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
146 post_signatures = global_id.signature_set.all()
146 post_signatures = global_id.signature_set.all()
147 if post_signatures:
147 if post_signatures:
148 signatures = post_signatures
148 signatures = post_signatures
149 else:
149 else:
150 key = KeyPair.objects.get(public_key=global_id.key)
150 key = KeyPair.objects.get(public_key=global_id.key)
151 signature = Signature(
151 signature = Signature(
152 key_type=key.key_type,
152 key_type=key.key_type,
153 key=key.public_key,
153 key=key.public_key,
154 signature=key.sign(global_id.content),
154 signature=key.sign(global_id.content),
155 global_id=global_id,
155 global_id=global_id,
156 )
156 )
157 signature.save()
157 signature.save()
158 signatures = [signature]
158 signatures = [signature]
159 for signature in signatures:
159 for signature in signatures:
160 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
160 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
161 signature_tag.set(ATTR_TYPE, signature.key_type)
161 signature_tag.set(ATTR_TYPE, signature.key_type)
162 signature_tag.set(ATTR_VALUE, signature.signature)
162 signature_tag.set(ATTR_VALUE, signature.signature)
163 signature_tag.set(ATTR_KEY, signature.key)
163 signature_tag.set(ATTR_KEY, signature.key)
164
164
165 return et.tostring(response, ENCODING_UNICODE)
165 return et.tostring(response, ENCODING_UNICODE)
166
166
167 @staticmethod
167 @staticmethod
168 @transaction.atomic
168 @transaction.atomic
169 def parse_response_get(response_xml, hostname):
169 def parse_response_get(response_xml, hostname):
170 tag_root = et.fromstring(response_xml)
170 tag_root = et.fromstring(response_xml)
171 tag_status = tag_root.find(TAG_STATUS)
171 tag_status = tag_root.find(TAG_STATUS)
172 if STATUS_SUCCESS == tag_status.text:
172 if STATUS_SUCCESS == tag_status.text:
173 tag_models = tag_root.find(TAG_MODELS)
173 tag_models = tag_root.find(TAG_MODELS)
174 for tag_model in tag_models:
174 for tag_model in tag_models:
175 tag_content = tag_model.find(TAG_CONTENT)
175 tag_content = tag_model.find(TAG_CONTENT)
176
176
177 content_str = et.tostring(tag_content, ENCODING_UNICODE)
177 content_str = et.tostring(tag_content, ENCODING_UNICODE)
178
178
179 tag_id = tag_content.find(TAG_ID)
179 tag_id = tag_content.find(TAG_ID)
180 global_id, exists = GlobalId.from_xml_element(tag_id)
180 global_id, exists = GlobalId.from_xml_element(tag_id)
181 signatures = SyncManager._verify_model(global_id, content_str, tag_model)
181 signatures = SyncManager._verify_model(global_id, content_str, tag_model)
182
182
183 version = int(tag_content.find(TAG_VERSION).text)
183 version = int(tag_content.find(TAG_VERSION).text)
184 is_old = exists and global_id.post.version < version
184 is_old = exists and global_id.post.version < version
185 if exists and not is_old:
185 if exists and not is_old:
186 print('Post with same ID exists and is up to date.')
186 print('Post with same ID exists and is up to date.')
187 else:
187 else:
188 global_id.content = content_str
188 global_id.content = content_str
189 global_id.save()
189 global_id.save()
190 for signature in signatures:
190 for signature in signatures:
191 signature.global_id = global_id
191 signature.global_id = global_id
192 signature.save()
192 signature.save()
193
193
194 title = tag_content.find(TAG_TITLE).text or ''
194 title = tag_content.find(TAG_TITLE).text or ''
195 text = tag_content.find(TAG_TEXT).text or ''
195 text = tag_content.find(TAG_TEXT).text or ''
196 pub_time = tag_content.find(TAG_PUB_TIME).text
196 pub_time = tag_content.find(TAG_PUB_TIME).text
197 tripcode_tag = tag_content.find(TAG_TRIPCODE)
197 tripcode_tag = tag_content.find(TAG_TRIPCODE)
198 if tripcode_tag is not None:
198 if tripcode_tag is not None:
199 tripcode = tripcode_tag.text or ''
199 tripcode = tripcode_tag.text or ''
200 else:
200 else:
201 tripcode = ''
201 tripcode = ''
202
202
203 thread = tag_content.find(TAG_THREAD)
203 thread = tag_content.find(TAG_THREAD)
204 tags = []
204 tags = []
205 if thread:
205 if thread:
206 thread_id = thread.find(TAG_ID)
206 thread_id = thread.find(TAG_ID)
207 op_global_id, exists = GlobalId.from_xml_element(thread_id)
207 op_global_id, exists = GlobalId.from_xml_element(thread_id)
208 if exists:
208 if exists:
209 opening_post = Post.objects.get(global_id=op_global_id)
209 opening_post = Post.objects.get(global_id=op_global_id)
210 else:
210 else:
211 raise SyncException(EXCEPTION_OP)
211 raise SyncException(EXCEPTION_OP)
212 else:
212 else:
213 opening_post = None
213 opening_post = None
214 tag_tags = tag_content.find(TAG_TAGS)
214 tag_tags = tag_content.find(TAG_TAGS)
215 for tag_tag in tag_tags:
215 for tag_tag in tag_tags:
216 tag, created = Tag.objects.get_or_create(
216 tag, created = Tag.objects.get_or_create(
217 name=tag_tag.text)
217 name=tag_tag.text)
218 tags.append(tag)
218 tags.append(tag)
219
219
220 # TODO Check that the replied posts are already present
220 # TODO Check that the replied posts are already present
221 # before adding new ones
221 # before adding new ones
222
222
223 files = []
223 files = []
224 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
224 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
225 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
225 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
226 for attachment in tag_attachments:
226 for attachment in tag_attachments:
227 tag_ref = tag_refs.find("{}[@ref='{}']".format(
227 tag_ref = tag_refs.find("{}[@ref='{}']".format(
228 TAG_ATTACHMENT_REF, attachment.text))
228 TAG_ATTACHMENT_REF, attachment.text))
229 url = tag_ref.get(ATTR_URL)
229 url = tag_ref.get(ATTR_URL)
230 attached_file = download(hostname + url)
230 attached_file = download(hostname + url)
231 if attached_file is None:
231 if attached_file is None:
232 raise SyncException(EXCEPTION_DOWNLOAD)
232 raise SyncException(EXCEPTION_DOWNLOAD)
233
233
234 hash = get_file_hash(attached_file)
234 hash = get_file_hash(attached_file)
235 if hash != attachment.text:
235 if hash != attachment.text:
236 raise SyncException(EXCEPTION_HASH)
236 raise SyncException(EXCEPTION_HASH)
237
237
238 files.append(attached_file)
238 files.append(attached_file)
239
239
240 if is_old:
240 if is_old:
241 post = global_id.post
241 post = global_id.post
242 Post.objects.update_post(
242 Post.objects.update_post(
243 post, title=title, text=text, pub_time=pub_time,
243 post, title=title, text=text, pub_time=pub_time,
244 tags=tags, files=files, tripcode=tripcode,
244 tags=tags, files=files, tripcode=tripcode,
245 version=version)
245 version=version)
246 logger.debug('Parsed updated post {}'.format(global_id))
246 logger.debug('Parsed updated post {}'.format(global_id))
247 else:
247 else:
248 Post.objects.import_post(
248 Post.objects.import_post(
249 title=title, text=text, pub_time=pub_time,
249 title=title, text=text, pub_time=pub_time,
250 opening_post=opening_post, tags=tags,
250 opening_post=opening_post, tags=tags,
251 global_id=global_id, files=files, tripcode=tripcode,
251 global_id=global_id, files=files, tripcode=tripcode,
252 version=version)
252 version=version)
253 logger.debug('Parsed new post {}'.format(global_id))
253 logger.debug('Parsed new post {}'.format(global_id))
254 else:
254 else:
255 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
255 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
256
256
257 @staticmethod
257 @staticmethod
258 def generate_response_list():
258 def generate_response_list():
259 response = et.Element(TAG_RESPONSE)
259 response = et.Element(TAG_RESPONSE)
260
260
261 status = et.SubElement(response, TAG_STATUS)
261 status = et.SubElement(response, TAG_STATUS)
262 status.text = STATUS_SUCCESS
262 status.text = STATUS_SUCCESS
263
263
264 models = et.SubElement(response, TAG_MODELS)
264 models = et.SubElement(response, TAG_MODELS)
265
265
266 for post in Post.objects.prefetch_related('global_id').all():
266 for post in Post.objects.prefetch_related('global_id').all():
267 tag_model = et.SubElement(models, TAG_MODEL)
267 tag_model = et.SubElement(models, TAG_MODEL)
268 tag_id = et.SubElement(tag_model, TAG_ID)
268 tag_id = et.SubElement(tag_model, TAG_ID)
269 post.global_id.to_xml_element(tag_id)
269 post.global_id.to_xml_element(tag_id)
270 tag_version = et.SubElement(tag_model, TAG_VERSION)
270 tag_version = et.SubElement(tag_model, TAG_VERSION)
271 tag_version.text = str(post.version)
271 tag_version.text = str(post.version)
272
272
273 return et.tostring(response, ENCODING_UNICODE)
273 return et.tostring(response, ENCODING_UNICODE)
274
274
275 @staticmethod
275 @staticmethod
276 def _verify_model(global_id, content_str, tag_model):
276 def _verify_model(global_id, content_str, tag_model):
277 """
277 """
278 Verifies all signatures for a single model.
278 Verifies all signatures for a single model.
279 """
279 """
280
280
281 signatures = []
281 signatures = []
282
282
283 tag_signatures = tag_model.find(TAG_SIGNATURES)
283 tag_signatures = tag_model.find(TAG_SIGNATURES)
284 has_author_signature = False
284 has_author_signature = False
285 for tag_signature in tag_signatures:
285 for tag_signature in tag_signatures:
286 signature_type = tag_signature.get(ATTR_TYPE)
286 signature_type = tag_signature.get(ATTR_TYPE)
287 signature_value = tag_signature.get(ATTR_VALUE)
287 signature_value = tag_signature.get(ATTR_VALUE)
288 signature_key = tag_signature.get(ATTR_KEY)
288 signature_key = tag_signature.get(ATTR_KEY)
289
289
290 if global_id.key_type == signature_type and\
290 if global_id.key_type == signature_type and\
291 global_id.key == signature_key:
291 global_id.key == signature_key:
292 has_author_signature = True
292 has_author_signature = True
293
293
294 signature = Signature(key_type=signature_type,
294 signature = Signature(key_type=signature_type,
295 key=signature_key,
295 key=signature_key,
296 signature=signature_value)
296 signature=signature_value)
297
297
298 if not KeyPair.objects.verify(signature, content_str):
298 if not KeyPair.objects.verify(signature, content_str):
299 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
299 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
300
300
301 signatures.append(signature)
301 signatures.append(signature)
302 if not has_author_signature:
302 if not has_author_signature:
303 raise SyncException(EXCEPTION_AUTHOR_SIGNATURE.format(content_str))
303 raise SyncException(EXCEPTION_AUTHOR_SIGNATURE.format(content_str))
304
304
305 return signatures
305 return signatures
306
306
307 @staticmethod
307 @staticmethod
308 def _attachment_to_xml(tag_attachments, tag_refs, attachment):
308 def _attachment_to_xml(tag_attachments, tag_refs, attachment):
309 if tag_attachments is not None:
309 if tag_attachments is not None:
310 attachment_tag = et.SubElement(tag_attachments, TAG_ATTACHMENT)
310 attachment_tag = et.SubElement(tag_attachments, TAG_ATTACHMENT)
311 if attachment.is_internal():
311 if attachment.is_internal():
312 mimetype = get_file_mimetype(attachment.file.file)
312 mimetype = get_file_mimetype(attachment.file.file)
313 attachment_tag.set(ATTR_MIMETYPE, mimetype)
313 attachment_tag.set(ATTR_MIMETYPE, mimetype)
314 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_MD5)
314 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_MD5)
315 attachment_tag.text = attachment.hash
315 attachment_tag.text = attachment.hash
316 else:
316 else:
317 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_URL)
317 attachment_tag.set(ATTR_ID_TYPE, ID_TYPE_URL)
318 attachment_tag.text = attachment.url
318 attachment_tag.text = attachment.url
319
319
320 if tag_refs is not None:
320 if tag_refs is not None:
321 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
321 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
322 attachment_ref.set(ATTR_REF, attachment.hash)
322 attachment_ref.set(ATTR_REF, attachment.hash)
323 attachment_ref.set(ATTR_URL, attachment.url)
323 attachment_ref.set(ATTR_URL, attachment.file.url)
General Comments 0
You need to be logged in to leave comments. Login now