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