##// END OF EJS Templates
Delete global ID when deleting post. Cache model's content XML tag into global ID
neko259 -
r1520:ecaafe92 decentral
parent child Browse files
Show More
@@ -1,251 +1,258 b''
1 1 import xml.etree.ElementTree as et
2 2
3 3 from boards.models.attachment.downloaders import download
4 4 from boards.utils import get_file_mimetype, get_file_hash
5 5 from django.db import transaction
6 6 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
7 7
8 8 ENCODING_UNICODE = 'unicode'
9 9
10 10 TAG_MODEL = 'model'
11 11 TAG_REQUEST = 'request'
12 12 TAG_RESPONSE = 'response'
13 13 TAG_ID = 'id'
14 14 TAG_STATUS = 'status'
15 15 TAG_MODELS = 'models'
16 16 TAG_TITLE = 'title'
17 17 TAG_TEXT = 'text'
18 18 TAG_THREAD = 'thread'
19 19 TAG_PUB_TIME = 'pub-time'
20 20 TAG_SIGNATURES = 'signatures'
21 21 TAG_SIGNATURE = 'signature'
22 22 TAG_CONTENT = 'content'
23 23 TAG_ATTACHMENTS = 'attachments'
24 24 TAG_ATTACHMENT = 'attachment'
25 25 TAG_TAGS = 'tags'
26 26 TAG_TAG = 'tag'
27 27 TAG_ATTACHMENT_REFS = 'attachment-refs'
28 28 TAG_ATTACHMENT_REF = 'attachment-ref'
29 29
30 30 TYPE_GET = 'get'
31 31
32 32 ATTR_VERSION = 'version'
33 33 ATTR_TYPE = 'type'
34 34 ATTR_NAME = 'name'
35 35 ATTR_VALUE = 'value'
36 36 ATTR_MIMETYPE = 'mimetype'
37 37 ATTR_KEY = 'key'
38 38 ATTR_REF = 'ref'
39 39 ATTR_URL = 'url'
40 40
41 41 STATUS_SUCCESS = 'success'
42 42
43 43
44 44 class SyncException(Exception):
45 45 pass
46 46
47 47
48 48 class SyncManager:
49 49 @staticmethod
50 50 def generate_response_get(model_list: list):
51 51 response = et.Element(TAG_RESPONSE)
52 52
53 53 status = et.SubElement(response, TAG_STATUS)
54 54 status.text = STATUS_SUCCESS
55 55
56 56 models = et.SubElement(response, TAG_MODELS)
57 57
58 # TODO Put global id's content into XML instad of manual serialization
59 58 for post in model_list:
60 59 model = et.SubElement(models, TAG_MODEL)
61 60 model.set(ATTR_NAME, 'post')
62 61
63 content_tag = et.SubElement(model, TAG_CONTENT)
64
65 tag_id = et.SubElement(content_tag, TAG_ID)
66 post.global_id.to_xml_element(tag_id)
67
68 title = et.SubElement(content_tag, TAG_TITLE)
69 title.text = post.title
62 global_id = post.global_id
70 63
71 text = et.SubElement(content_tag, TAG_TEXT)
72 text.text = post.get_sync_text()
73
74 thread = post.get_thread()
75 if post.is_opening():
76 tag_tags = et.SubElement(content_tag, TAG_TAGS)
77 for tag in thread.get_tags():
78 tag_tag = et.SubElement(tag_tags, TAG_TAG)
79 tag_tag.text = tag.name
64 if global_id.content:
65 model.append(et.fromstring(global_id.content))
80 66 else:
81 tag_thread = et.SubElement(content_tag, TAG_THREAD)
82 thread_id = et.SubElement(tag_thread, TAG_ID)
83 thread.get_opening_post().global_id.to_xml_element(thread_id)
67 content_tag = et.SubElement(model, TAG_CONTENT)
68
69 tag_id = et.SubElement(content_tag, TAG_ID)
70 global_id.to_xml_element(tag_id)
84 71
85 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
86 pub_time.text = str(post.get_pub_time_str())
72 title = et.SubElement(content_tag, TAG_TITLE)
73 title.text = post.title
74
75 text = et.SubElement(content_tag, TAG_TEXT)
76 text.text = post.get_sync_text()
87 77
88 images = post.images.all()
89 attachments = post.attachments.all()
90 if len(images) > 0 or len(attachments) > 0:
91 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
92 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
78 thread = post.get_thread()
79 if post.is_opening():
80 tag_tags = et.SubElement(content_tag, TAG_TAGS)
81 for tag in thread.get_tags():
82 tag_tag = et.SubElement(tag_tags, TAG_TAG)
83 tag_tag.text = tag.name
84 else:
85 tag_thread = et.SubElement(content_tag, TAG_THREAD)
86 thread_id = et.SubElement(tag_thread, TAG_ID)
87 thread.get_opening_post().global_id.to_xml_element(thread_id)
88
89 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
90 pub_time.text = str(post.get_pub_time_str())
93 91
94 for image in images:
95 SyncManager._attachment_to_xml(
96 attachments_tag, attachment_refs, image.image.file,
97 image.hash, image.image.url)
98 for file in attachments:
99 SyncManager._attachment_to_xml(
100 attachments_tag, attachment_refs, file.file.file,
101 file.hash, file.file.url)
92 images = post.images.all()
93 attachments = post.attachments.all()
94 if len(images) > 0 or len(attachments) > 0:
95 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
96 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
97
98 for image in images:
99 SyncManager._attachment_to_xml(
100 attachments_tag, attachment_refs, image.image.file,
101 image.hash, image.image.url)
102 for file in attachments:
103 SyncManager._attachment_to_xml(
104 attachments_tag, attachment_refs, file.file.file,
105 file.hash, file.file.url)
106
107 global_id.content = et.tostring(content_tag, ENCODING_UNICODE)
108 global_id.save()
102 109
103 110 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
104 post_signatures = post.global_id.signature_set.all()
111 post_signatures = global_id.signature_set.all()
105 112 if post_signatures:
106 113 signatures = post_signatures
107 114 else:
108 key = KeyPair.objects.get(public_key=post.global_id.key)
115 key = KeyPair.objects.get(public_key=global_id.key)
109 116 signature = Signature(
110 117 key_type=key.key_type,
111 118 key=key.public_key,
112 signature=key.sign(et.tostring(content_tag, encoding=ENCODING_UNICODE)),
113 global_id=post.global_id,
119 signature=key.sign(global_id.content),
120 global_id=global_id,
114 121 )
115 122 signature.save()
116 123 signatures = [signature]
117 124 for signature in signatures:
118 125 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
119 126 signature_tag.set(ATTR_TYPE, signature.key_type)
120 127 signature_tag.set(ATTR_VALUE, signature.signature)
121 128 signature_tag.set(ATTR_KEY, signature.key)
122 129
123 130 return et.tostring(response, ENCODING_UNICODE)
124 131
125 132 @staticmethod
126 133 @transaction.atomic
127 134 def parse_response_get(response_xml, hostname):
128 135 tag_root = et.fromstring(response_xml)
129 136 tag_status = tag_root.find(TAG_STATUS)
130 137 if STATUS_SUCCESS == tag_status.text:
131 138 tag_models = tag_root.find(TAG_MODELS)
132 139 for tag_model in tag_models:
133 140 tag_content = tag_model.find(TAG_CONTENT)
134 141
135 142 signatures = SyncManager._verify_model(tag_content, tag_model)
136 143
137 144 tag_id = tag_content.find(TAG_ID)
138 145 global_id, exists = GlobalId.from_xml_element(tag_id)
139 146
140 147 if exists:
141 148 print('Post with same ID already exists')
142 149 else:
143 global_id.content = et.to_string(tag_content,
144 ENCODING_UNICODE)
150 global_id.content = et.tostring(tag_content,
151 ENCODING_UNICODE)
145 152 global_id.save()
146 153 for signature in signatures:
147 154 signature.global_id = global_id
148 155 signature.save()
149 156
150 157 title = tag_content.find(TAG_TITLE).text or ''
151 158 text = tag_content.find(TAG_TEXT).text or ''
152 159 pub_time = tag_content.find(TAG_PUB_TIME).text
153 160
154 161 thread = tag_content.find(TAG_THREAD)
155 162 tags = []
156 163 if thread:
157 164 thread_id = thread.find(TAG_ID)
158 165 op_global_id, exists = GlobalId.from_xml_element(thread_id)
159 166 if exists:
160 167 opening_post = Post.objects.get(global_id=op_global_id)
161 168 else:
162 169 raise SyncException('Load the OP first')
163 170 else:
164 171 opening_post = None
165 172 tag_tags = tag_content.find(TAG_TAGS)
166 173 for tag_tag in tag_tags:
167 174 tag, created = Tag.objects.get_or_create(
168 175 name=tag_tag.text)
169 176 tags.append(tag)
170 177
171 178 # TODO Check that the replied posts are already present
172 179 # before adding new ones
173 180
174 181 files = []
175 182 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
176 183 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
177 184 for attachment in tag_attachments:
178 185 tag_ref = tag_refs.find("{}[@ref='{}']".format(
179 186 TAG_ATTACHMENT_REF, attachment.text))
180 187 url = tag_ref.get(ATTR_URL)
181 188 attached_file = download(hostname + url)
182 189 if attached_file is None:
183 190 raise SyncException('File was not dowloaded')
184 191
185 192 hash = get_file_hash(file)
186 193 if hash != attachment.text:
187 194 raise SyncException('File hash does not match attachment hash')
188 195
189 196 files.append(attached_file)
190 197
191 198 Post.objects.import_post(
192 199 title=title, text=text, pub_time=pub_time,
193 200 opening_post=opening_post, tags=tags,
194 201 global_id=global_id, files=files)
195 202 else:
196 203 raise SyncException('Sync node returned an error: {}'.format(
197 204 tag_status.text))
198 205
199 206 @staticmethod
200 207 def generate_response_pull():
201 208 response = et.Element(TAG_RESPONSE)
202 209
203 210 status = et.SubElement(response, TAG_STATUS)
204 211 status.text = STATUS_SUCCESS
205 212
206 213 models = et.SubElement(response, TAG_MODELS)
207 214
208 215 for post in Post.objects.all():
209 216 tag_id = et.SubElement(models, TAG_ID)
210 217 post.global_id.to_xml_element(tag_id)
211 218
212 219 return et.tostring(response, ENCODING_UNICODE)
213 220
214 221 @staticmethod
215 222 def _verify_model(tag_content, tag_model):
216 223 """
217 224 Verifies all signatures for a single model.
218 225 """
219 226
220 227 signatures = []
221 228
222 229 tag_signatures = tag_model.find(TAG_SIGNATURES)
223 230 for tag_signature in tag_signatures:
224 231 signature_type = tag_signature.get(ATTR_TYPE)
225 232 signature_value = tag_signature.get(ATTR_VALUE)
226 233 signature_key = tag_signature.get(ATTR_KEY)
227 234
228 235 signature = Signature(key_type=signature_type,
229 236 key=signature_key,
230 237 signature=signature_value)
231 238
232 239 content = et.tostring(tag_content, ENCODING_UNICODE)
233 240
234 241 if not KeyPair.objects.verify(
235 242 signature, content):
236 243 raise SyncException('Invalid model signature for {}'.format(content))
237 244
238 245 signatures.append(signature)
239 246
240 247 return signatures
241 248
242 249 @staticmethod
243 250 def _attachment_to_xml(tag_attachments, tag_refs, file, hash, url):
244 251 mimetype = get_file_mimetype(file)
245 252 attachment = et.SubElement(tag_attachments, TAG_ATTACHMENT)
246 253 attachment.set(ATTR_MIMETYPE, mimetype)
247 254 attachment.text = hash
248 255
249 256 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
250 257 attachment_ref.set(ATTR_REF, hash)
251 258 attachment_ref.set(ATTR_URL, url)
@@ -1,138 +1,144 b''
1 1 import xml.etree.ElementTree as et
2 2 from django.db import models
3 3
4 4
5 5 TAG_MODEL = 'model'
6 6 TAG_REQUEST = 'request'
7 7 TAG_ID = 'id'
8 8
9 9 TYPE_GET = 'get'
10 10 TYPE_PULL = 'pull'
11 11
12 12 ATTR_VERSION = 'version'
13 13 ATTR_TYPE = 'type'
14 14 ATTR_NAME = 'name'
15 15
16 16 ATTR_KEY = 'key'
17 17 ATTR_KEY_TYPE = 'type'
18 18 ATTR_LOCAL_ID = 'local-id'
19 19
20 20
21 21 class GlobalIdManager(models.Manager):
22 22 def generate_request_get(self, global_id_list: list):
23 23 """
24 24 Form a get request from a list of ModelId objects.
25 25 """
26 26
27 27 request = et.Element(TAG_REQUEST)
28 28 request.set(ATTR_TYPE, TYPE_GET)
29 29 request.set(ATTR_VERSION, '1.0')
30 30
31 31 model = et.SubElement(request, TAG_MODEL)
32 32 model.set(ATTR_VERSION, '1.0')
33 33 model.set(ATTR_NAME, 'post')
34 34
35 35 for global_id in global_id_list:
36 36 tag_id = et.SubElement(model, TAG_ID)
37 37 global_id.to_xml_element(tag_id)
38 38
39 39 return et.tostring(request, 'unicode')
40 40
41 41 def generate_request_pull(self):
42 42 """
43 43 Form a pull request from a list of ModelId objects.
44 44 """
45 45
46 46 request = et.Element(TAG_REQUEST)
47 47 request.set(ATTR_TYPE, TYPE_PULL)
48 48 request.set(ATTR_VERSION, '1.0')
49 49
50 50 model = et.SubElement(request, TAG_MODEL)
51 51 model.set(ATTR_VERSION, '1.0')
52 52 model.set(ATTR_NAME, 'post')
53 53
54 54 return et.tostring(request, 'unicode')
55 55
56 56 def global_id_exists(self, global_id):
57 57 """
58 58 Checks if the same global id already exists in the system.
59 59 """
60 60
61 61 return self.filter(key=global_id.key,
62 62 key_type=global_id.key_type,
63 63 local_id=global_id.local_id).exists()
64 64
65 65
66 66 class GlobalId(models.Model):
67 """
68 Global model ID and cache.
69 Key, key type and local ID make a single global identificator of the model.
70 Content is an XML cache of the model that can be passed along between nodes
71 without manual serialization each time.
72 """
67 73 class Meta:
68 74 app_label = 'boards'
69 75
70 76 objects = GlobalIdManager()
71 77
72 78 def __init__(self, *args, **kwargs):
73 79 models.Model.__init__(self, *args, **kwargs)
74 80
75 81 if 'key' in kwargs and 'key_type' in kwargs and 'local_id' in kwargs:
76 82 self.key = kwargs['key']
77 83 self.key_type = kwargs['key_type']
78 84 self.local_id = kwargs['local_id']
79 85
80 86 key = models.TextField()
81 87 key_type = models.TextField()
82 88 local_id = models.IntegerField()
83 89 content = models.TextField(blank=True, null=True)
84 90
85 91 def __str__(self):
86 92 return '%s::%s::%d' % (self.key_type, self.key, self.local_id)
87 93
88 94 def to_xml_element(self, element: et.Element):
89 95 """
90 96 Exports global id to an XML element.
91 97 """
92 98
93 99 element.set(ATTR_KEY, self.key)
94 100 element.set(ATTR_KEY_TYPE, self.key_type)
95 101 element.set(ATTR_LOCAL_ID, str(self.local_id))
96 102
97 103 @staticmethod
98 104 def from_xml_element(element: et.Element):
99 105 """
100 106 Parses XML id tag and gets global id from it.
101 107
102 108 Arguments:
103 109 element -- the XML 'id' element
104 110
105 111 Returns:
106 112 global_id -- id itself
107 113 exists -- True if the global id was taken from database, False if it
108 114 did not exist and was created.
109 115 """
110 116
111 117 try:
112 118 return GlobalId.objects.get(key=element.get(ATTR_KEY),
113 119 key_type=element.get(ATTR_KEY_TYPE),
114 120 local_id=int(element.get(
115 121 ATTR_LOCAL_ID))), True
116 122 except GlobalId.DoesNotExist:
117 123 return GlobalId(key=element.get(ATTR_KEY),
118 124 key_type=element.get(ATTR_KEY_TYPE),
119 125 local_id=int(element.get(ATTR_LOCAL_ID))), False
120 126
121 127
122 128 class Signature(models.Model):
123 129 class Meta:
124 130 app_label = 'boards'
125 131
126 132 def __init__(self, *args, **kwargs):
127 133 models.Model.__init__(self, *args, **kwargs)
128 134
129 135 if 'key' in kwargs and 'key_type' in kwargs and 'signature' in kwargs:
130 136 self.key_type = kwargs['key_type']
131 137 self.key = kwargs['key']
132 138 self.signature = kwargs['signature']
133 139
134 140 key_type = models.TextField()
135 141 key = models.TextField()
136 142 signature = models.TextField()
137 143
138 144 global_id = models.ForeignKey('GlobalId')
@@ -1,81 +1,87 b''
1 1 import re
2 2 from boards.mdx_neboard import get_parser
3 3
4 4 from boards.models import Post, GlobalId
5 5 from boards.models.post import REGEX_NOTIFICATION
6 6 from boards.models.post import REGEX_REPLY, REGEX_GLOBAL_REPLY
7 7 from boards.models.user import Notification
8 8 from django.db.models.signals import post_save, pre_save, pre_delete, \
9 9 post_delete
10 10 from django.dispatch import receiver
11 11 from django.utils import timezone
12 12
13 13
14 14 @receiver(post_save, sender=Post)
15 15 def connect_replies(instance, **kwargs):
16 16 for reply_number in re.finditer(REGEX_REPLY, instance.get_raw_text()):
17 17 post_id = reply_number.group(1)
18 18
19 19 try:
20 20 referenced_post = Post.objects.get(id=post_id)
21 21
22 22 referenced_post.referenced_posts.add(instance)
23 23 referenced_post.last_edit_time = instance.pub_time
24 24 referenced_post.build_refmap()
25 25 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
26 26 except Post.ObjectDoesNotExist:
27 27 pass
28 28
29 29
30 30 @receiver(post_save, sender=Post)
31 31 def connect_global_replies(instance, **kwargs):
32 32 for reply_number in re.finditer(REGEX_GLOBAL_REPLY, instance.get_raw_text()):
33 33 key_type = reply_number.group(1)
34 34 key = reply_number.group(2)
35 35 local_id = reply_number.group(3)
36 36
37 37 try:
38 38 global_id = GlobalId.objects.get(key_type=key_type, key=key,
39 39 local_id=local_id)
40 40 referenced_post = Post.objects.get(global_id=global_id)
41 41 referenced_post.referenced_posts.add(instance)
42 42 referenced_post.last_edit_time = instance.pub_time
43 43 referenced_post.build_refmap()
44 44 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
45 45 except (GlobalId.ObjectDoesNotExist, Post.ObjectDoesNotExist):
46 46 pass
47 47
48 48
49 49 @receiver(post_save, sender=Post)
50 50 def connect_notifications(instance, **kwargs):
51 51 for reply_number in re.finditer(REGEX_NOTIFICATION, instance.get_raw_text()):
52 52 user_name = reply_number.group(1).lower()
53 53 Notification.objects.get_or_create(name=user_name, post=instance)
54 54
55 55
56 56 @receiver(pre_save, sender=Post)
57 57 def preparse_text(instance, **kwargs):
58 58 instance._text_rendered = get_parser().parse(instance.get_raw_text())
59 59
60 60
61 61 @receiver(pre_delete, sender=Post)
62 62 def delete_images(instance, **kwargs):
63 63 for image in instance.images.all():
64 64 image_refs_count = image.post_images.count()
65 65 if image_refs_count == 1:
66 66 image.delete()
67 67
68 68
69 69 @receiver(pre_delete, sender=Post)
70 70 def delete_attachments(instance, **kwargs):
71 71 for attachment in instance.attachments.all():
72 72 attachment_refs_count = attachment.attachment_posts.count()
73 73 if attachment_refs_count == 1:
74 74 attachment.delete()
75 75
76 76
77 77 @receiver(post_delete, sender=Post)
78 78 def update_thread_on_delete(instance, **kwargs):
79 79 thread = instance.get_thread()
80 80 thread.last_edit_time = timezone.now()
81 81 thread.save()
82
83
84 @receiver(post_delete, sender=Post)
85 def delete_global_id(instance, **kwargs):
86 if instance.global_id and instance.global_id.id:
87 instance.global_id.delete()
General Comments 0
You need to be logged in to leave comments. Login now