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