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