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