##// END OF EJS Templates
Try all chunks when detecting file mimetype
neko259 -
r1869:f74167a6 default
parent child Browse files
Show More
@@ -1,239 +1,240 b''
1 1 import re
2 2
3 3 from PIL import Image
4 4
5 5 from django.contrib.staticfiles import finders
6 6 from django.contrib.staticfiles.templatetags.staticfiles import static
7 7 from django.core.files.images import get_image_dimensions
8 8 from django.template.defaultfilters import filesizeformat
9 9 from django.core.urlresolvers import reverse
10 10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11 11
12 12 from boards.utils import get_domain, cached_result
13 13 from boards import settings
14 14
15 15
16 16 FILE_STUB_IMAGE = 'images/file.png'
17 17 FILE_STUB_URL = 'url'
18 18 FILE_FILEFORMAT = 'images/fileformats/{}.png'
19 19
20 20
21 21 FILE_TYPES_VIDEO = (
22 22 'video/webm',
23 23 'video/mp4',
24 24 'video/mpeg',
25 25 'video/ogv',
26 26 )
27 27 FILE_TYPE_SVG = 'image/svg'
28 28 FILE_TYPES_AUDIO = (
29 29 'audio/ogg',
30 30 'audio/mp3',
31 31 'audio/opus',
32 32 'audio/x-flac',
33 'audio/mpeg',
33 34 )
34 35 FILE_TYPES_IMAGE = (
35 36 'image/jpeg',
36 37 'image/jpg',
37 38 'image/png',
38 39 'image/bmp',
39 40 'image/gif',
40 41 )
41 42
42 43 PLAIN_FILE_FORMATS = {
43 44 'application/zip': 'archive',
44 45 'application/x-tar': 'archive',
45 46 'gz': 'archive',
46 47 'audio/midi' : 'midi',
47 48 }
48 49
49 50 URL_PROTOCOLS = {
50 51 'magnet': 'magnet',
51 52 }
52 53
53 54 CSS_CLASS_IMAGE = 'image'
54 55 CSS_CLASS_THUMB = 'thumb'
55 56
56 57 ABSTRACT_VIEW = '<div class="image">'\
57 58 '{}'\
58 59 '<div class="image-metadata"><a href="{}" download >{}, {}</a>'\
59 60 ' <a class="file-menu" href="#" data-type="{}" data-search-url="{}" data-filename="{}">πŸ” </a></div>'\
60 61 '</div>'
61 62 URL_VIEW = '<div class="image">' \
62 63 '{}' \
63 64 '<div class="image-metadata">{}</div>' \
64 65 '</div>'
65 66 ABSTRACT_FORMAT_VIEW = '<a href="{}">'\
66 67 '<img class="url-image" src="{}" width="{}" height="{}"/>'\
67 68 '</a>'
68 69 VIDEO_FORMAT_VIEW = '<video width="200" height="150" controls src="{}"></video>'
69 70 AUDIO_FORMAT_VIEW = '<audio controls src="{}"></audio>'
70 71 IMAGE_FORMAT_VIEW = '<a class="{}" href="{full}">' \
71 72 '<img class="post-image-preview"' \
72 73 ' src="{}"' \
73 74 ' alt="{}"' \
74 75 ' width="{}"' \
75 76 ' height="{}"' \
76 77 ' data-width="{}"' \
77 78 ' data-height="{}" />' \
78 79 '</a>'
79 80 SVG_FORMAT_VIEW = '<a class="thumb" href="{}">'\
80 81 '<img class="post-image-preview" width="200" height="150" src="{}" />'\
81 82 '</a>'
82 83 URL_FORMAT_VIEW = '<a href="{}">' \
83 84 '<img class="url-image" src="{}" width="{}" height="{}"/>' \
84 85 '</a>'
85 86
86 87
87 88 def get_viewers():
88 89 return AbstractViewer.__subclasses__()
89 90
90 91
91 92 def get_static_dimensions(filename):
92 93 file_path = finders.find(filename)
93 94 return get_image_dimensions(file_path)
94 95
95 96
96 97 # TODO Move this to utils
97 98 def file_exists(filename):
98 99 return finders.find(filename) is not None
99 100
100 101
101 102 class AbstractViewer:
102 103 def __init__(self, file, file_type, hash, url):
103 104 self.file = file
104 105 self.file_type = file_type
105 106 self.hash = hash
106 107 self.url = url
107 108
108 109 @staticmethod
109 110 def supports(file_type):
110 111 return True
111 112
112 113 def get_view(self):
113 114 search_host = settings.get('External', 'ImageSearchHost')
114 115 if search_host:
115 116 if search_host.endswith('/'):
116 117 search_host = search_host[:-1]
117 118 search_url = search_host + self.file.url
118 119 else:
119 120 search_url = ''
120 121
121 122 return ABSTRACT_VIEW.format(self.get_format_view(), self.file.url,
122 123 self.file_type, filesizeformat(self.file.size),
123 124 self.file_type, search_url, self.file.name)
124 125
125 126 def get_format_view(self):
126 127 image_name = PLAIN_FILE_FORMATS.get(self.file_type, self.file_type)
127 128 file_name = FILE_FILEFORMAT.format(image_name)
128 129
129 130 if file_exists(file_name):
130 131 image = file_name
131 132 else:
132 133 image = FILE_STUB_IMAGE
133 134
134 135 w, h = get_static_dimensions(image)
135 136
136 137 return ABSTRACT_FORMAT_VIEW.format(self.file.url, static(image), w, h)
137 138
138 139
139 140 class VideoViewer(AbstractViewer):
140 141 @staticmethod
141 142 def supports(file_type):
142 143 return file_type in FILE_TYPES_VIDEO
143 144
144 145 def get_format_view(self):
145 146 return VIDEO_FORMAT_VIEW.format(self.file.url)
146 147
147 148
148 149 class AudioViewer(AbstractViewer):
149 150 @staticmethod
150 151 def supports(file_type):
151 152 return file_type in FILE_TYPES_AUDIO
152 153
153 154 def get_format_view(self):
154 155 return AUDIO_FORMAT_VIEW.format(self.file.url)
155 156
156 157
157 158 class SvgViewer(AbstractViewer):
158 159 @staticmethod
159 160 def supports(file_type):
160 161 return file_type == FILE_TYPE_SVG
161 162
162 163 def get_format_view(self):
163 164 return SVG_FORMAT_VIEW.format(self.file.url, self.file.url)
164 165
165 166
166 167 class ImageViewer(AbstractViewer):
167 168 @staticmethod
168 169 def supports(file_type):
169 170 return file_type in FILE_TYPES_IMAGE
170 171
171 172 def get_format_view(self):
172 173 metadata = '{}, {}'.format(self.file.name.split('.')[-1],
173 174 filesizeformat(self.file.size))
174 175
175 176 try:
176 177 width, height = get_image_dimensions(self.file.path)
177 178 except Exception:
178 179 # If the image is a decompression bomb, treat it as just a regular
179 180 # file
180 181 return super().get_format_view()
181 182
182 183 preview_path = self.file.path.replace('.', '.200x150.')
183 184 pre_width, pre_height = get_image_dimensions(preview_path)
184 185
185 186 split = self.file.url.rsplit('.', 1)
186 187 w, h = 200, 150
187 188 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
188 189
189 190 return IMAGE_FORMAT_VIEW.format(CSS_CLASS_THUMB,
190 191 thumb_url,
191 192 self.hash,
192 193 str(pre_width),
193 194 str(pre_height), str(width), str(height),
194 195 full=self.file.url, image_meta=metadata)
195 196
196 197
197 198 class UrlViewer(AbstractViewer):
198 199 @staticmethod
199 200 def supports(file_type):
200 201 return file_type is None
201 202
202 203 def get_view(self):
203 204 return URL_VIEW.format(self.get_format_view(), get_domain(self.url))
204 205
205 206 def get_format_view(self):
206 207 protocol = self.url.split(':')[0]
207 208
208 209 domain = get_domain(self.url)
209 210
210 211 if protocol in URL_PROTOCOLS:
211 212 url_image_name = URL_PROTOCOLS.get(protocol)
212 213 elif domain:
213 214 url_image_name = self._find_image_for_domains(domain) or FILE_STUB_URL
214 215 else:
215 216 url_image_name = FILE_STUB_URL
216 217
217 218 image_path = 'images/{}.png'.format(url_image_name)
218 219 image = static(image_path)
219 220 w, h = get_static_dimensions(image_path)
220 221
221 222 return URL_FORMAT_VIEW.format(self.url, image, w, h)
222 223
223 224 @cached_result()
224 225 def _find_image_for_domains(self, domain):
225 226 """
226 227 Searches for the domain image for every domain level except top.
227 228 E.g. for l3.example.co.uk it will search for l3.example.co.uk, then
228 229 example.co.uk, then co.uk
229 230 """
230 231 levels = domain.split('.')
231 232 while len(levels) > 1:
232 233 domain = '.'.join(levels)
233 234
234 235 filename = 'images/domains/{}.png'.format(domain)
235 236 if file_exists(filename):
236 237 return 'domains/' + domain
237 238 else:
238 239 del levels[0]
239 240
@@ -1,157 +1,157 b''
1 1 """
2 2 This module contains helper functions and helper classes.
3 3 """
4 4 import hashlib
5 5 import uuid
6 6
7 7 from boards.abstracts.constants import FILE_DIRECTORY
8 8 from random import random
9 9 import time
10 10 import hmac
11 11
12 12 from django.core.cache import cache
13 13 from django.db.models import Model
14 14 from django import forms
15 15 from django.template.defaultfilters import filesizeformat
16 16 from django.utils import timezone
17 17 from django.utils.translation import ugettext_lazy as _
18 18 import magic
19 19 import os
20 20
21 21 import boards
22 22 from boards.settings import get_bool
23 23 from neboard import settings
24 24
25 25 CACHE_KEY_DELIMITER = '_'
26 26
27 27 HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR'
28 28 META_REMOTE_ADDR = 'REMOTE_ADDR'
29 29
30 30 SETTING_MESSAGES = 'Messages'
31 31 SETTING_ANON_MODE = 'AnonymousMode'
32 32
33 33 ANON_IP = '127.0.0.1'
34 34
35 35 FILE_EXTENSION_DELIMITER = '.'
36 36
37 37
38 38 def is_anonymous_mode():
39 39 return get_bool(SETTING_MESSAGES, SETTING_ANON_MODE)
40 40
41 41
42 42 def get_client_ip(request):
43 43 if is_anonymous_mode():
44 44 ip = ANON_IP
45 45 else:
46 46 x_forwarded_for = request.META.get(HTTP_FORWARDED)
47 47 if x_forwarded_for:
48 48 ip = x_forwarded_for.split(',')[-1].strip()
49 49 else:
50 50 ip = request.META.get(META_REMOTE_ADDR)
51 51 return ip
52 52
53 53
54 54 # TODO The output format is not epoch because it includes microseconds
55 55 def datetime_to_epoch(datetime):
56 56 return int(time.mktime(timezone.localtime(
57 57 datetime,timezone.get_current_timezone()).timetuple())
58 58 * 1000000 + datetime.microsecond)
59 59
60 60
61 61 def get_websocket_token(user_id='', timestamp=''):
62 62 """
63 63 Create token to validate information provided by new connection.
64 64 """
65 65
66 66 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
67 67 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
68 68 sign.update(user_id.encode())
69 69 sign.update(timestamp.encode())
70 70 token = sign.hexdigest()
71 71
72 72 return token
73 73
74 74
75 75 # TODO Test this carefully
76 76 def cached_result(key_method=None):
77 77 """
78 78 Caches method result in the Django's cache system, persisted by object name,
79 79 object name, model id if object is a Django model, args and kwargs if any.
80 80 """
81 81 def _cached_result(function):
82 82 def inner_func(obj, *args, **kwargs):
83 83 cache_key_params = [obj.__class__.__name__, function.__name__]
84 84
85 85 cache_key_params += args
86 86 for key, value in kwargs:
87 87 cache_key_params.append(key + ':' + value)
88 88
89 89 if isinstance(obj, Model):
90 90 cache_key_params.append(str(obj.id))
91 91
92 92 if key_method is not None:
93 93 cache_key_params += [str(arg) for arg in key_method(obj)]
94 94
95 95 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
96 96
97 97 persisted_result = cache.get(cache_key)
98 98 if persisted_result is not None:
99 99 result = persisted_result
100 100 else:
101 101 result = function(obj, *args, **kwargs)
102 102 if result is not None:
103 103 cache.set(cache_key, result)
104 104
105 105 return result
106 106
107 107 return inner_func
108 108 return _cached_result
109 109
110 110
111 111 def get_file_hash(file) -> str:
112 112 md5 = hashlib.md5()
113 113 for chunk in file.chunks():
114 114 md5.update(chunk)
115 115 return md5.hexdigest()
116 116
117 117
118 118 def validate_file_size(size: int):
119 119 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
120 120 if max_size > 0 and size > max_size:
121 121 raise forms.ValidationError(
122 122 _('File must be less than %s but is %s.')
123 123 % (filesizeformat(max_size), filesizeformat(size)))
124 124
125 125
126 126 def get_extension(filename):
127 127 return filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
128 128
129 129
130 130 def get_upload_filename(model_instance, old_filename):
131 131 extension = get_extension(old_filename)
132 132 new_name = '{}.{}'.format(uuid.uuid4(), extension)
133 133
134 134 return os.path.join(FILE_DIRECTORY, new_name)
135 135
136 136
137 137 def get_file_mimetype(file) -> str:
138 file_type = magic.from_buffer(file.chunks().__next__(), mime=True)
138 file_type = magic.from_file(settings.MEDIA_ROOT + file.name, mime=True)
139 139 if file_type is None:
140 140 file_type = 'application/octet-stream'
141 141 elif type(file_type) == bytes:
142 142 file_type = file_type.decode()
143 143 return file_type
144 144
145 145
146 146 def get_domain(url: str) -> str:
147 147 """
148 148 Gets domain from an URL with random number of domain levels.
149 149 """
150 150 domain_parts = url.split('/')
151 151 if len(domain_parts) >= 2:
152 152 full_domain = domain_parts[2]
153 153 else:
154 154 full_domain = ''
155 155
156 156 return full_domain
157 157
General Comments 0
You need to be logged in to leave comments. Login now