Commit 6deb4a4fe31ff9b6b386f6524142926730265bd8
1 parent
ea4ff4d2
Exists in
master
and in
1 other branch
- file support added. Every file in public subdir of some topic is accessible th…
…rough /file/filename. In the question text images can be referred in markdown as `` - files are served asynchronously in chunks of 1MiB.
Showing
4 changed files
with
46 additions
and
36 deletions
Show diff stats
BUGS.md
@@ -11,7 +11,6 @@ | @@ -11,7 +11,6 @@ | ||
11 | 11 | ||
12 | # TODO | 12 | # TODO |
13 | 13 | ||
14 | -- servir imagens/ficheiros. | ||
15 | - session management. close after inactive time. | 14 | - session management. close after inactive time. |
16 | - each topic only loads a sample of K questions (max) in random order. | 15 | - each topic only loads a sample of K questions (max) in random order. |
17 | - radio e checkboxes, aceitar numeros como seleccao das opcoes. | 16 | - radio e checkboxes, aceitar numeros como seleccao das opcoes. |
@@ -30,6 +29,7 @@ | @@ -30,6 +29,7 @@ | ||
30 | 29 | ||
31 | # FIXED | 30 | # FIXED |
32 | 31 | ||
32 | +- servir imagens/ficheiros. | ||
33 | - radio: suporte para multiplas opcoes correctas e erradas, escolhendo-se uma selecção aleatoria destas (so com 1 certa). | 33 | - radio: suporte para multiplas opcoes correctas e erradas, escolhendo-se uma selecção aleatoria destas (so com 1 certa). |
34 | - checkbox: cada opção pode ser uma dupla (certo, errado) sendo escolhida uma aleatória. | 34 | - checkbox: cada opção pode ser uma dupla (certo, errado) sendo escolhida uma aleatória. |
35 | - async/threadpool no bcrypt do initdb. | 35 | - async/threadpool no bcrypt do initdb. |
demo/solar_system/questions.yaml
1 | --- | 1 | --- |
2 | 2 | ||
3 | -# # --------------------------------------------------------------------------- | ||
4 | -# - | ||
5 | -# ref: solar-system | ||
6 | -# type: radio | ||
7 | -# title: Sistema solar | ||
8 | -# text: Qual é o maior planeta do Sistema Solar? | ||
9 | -# options: | ||
10 | -# - Mercúrio | ||
11 | -# - Marte | ||
12 | -# - Júpiter | ||
13 | -# - Têm todos o mesmo tamanho | ||
14 | -# # opcional | ||
15 | -# correct: 2 | ||
16 | -# shuffle: False | ||
17 | -# discount: True | 3 | +# --------------------------------------------------------------------------- |
4 | +- | ||
5 | + ref: solar-system | ||
6 | + type: radio | ||
7 | + title: Sistema solar | ||
8 | + text: | | ||
9 | +  | ||
10 | + | ||
11 | + Qual é o maior planeta do Sistema Solar? | ||
12 | + options: | ||
13 | + - Mercúrio | ||
14 | + - Marte | ||
15 | + - Júpiter | ||
16 | + - Têm todos o mesmo tamanho | ||
17 | + # opcional | ||
18 | + correct: 2 | ||
19 | + shuffle: False | ||
20 | + discount: True | ||
18 | 21 | ||
19 | # # --------------------------------------------------------------------------- | 22 | # # --------------------------------------------------------------------------- |
20 | # - | 23 | # - |
serve.py
@@ -30,8 +30,8 @@ class WebApplication(tornado.web.Application): | @@ -30,8 +30,8 @@ class WebApplication(tornado.web.Application): | ||
30 | (r'/change_password', ChangePasswordHandler), | 30 | (r'/change_password', ChangePasswordHandler), |
31 | (r'/question', QuestionHandler), # each question | 31 | (r'/question', QuestionHandler), # each question |
32 | (r'/topic/(.+)', TopicHandler), # page for doing a topic | 32 | (r'/topic/(.+)', TopicHandler), # page for doing a topic |
33 | - # (r'/file/(.+)', FileHandler), # FIXME | ||
34 | - (r'/.*', RootHandler), # show list of topics | 33 | + (r'/file/(.+)', FileHandler), # FIXME |
34 | + (r'/', RootHandler), # show list of topics | ||
35 | ] | 35 | ] |
36 | settings = { | 36 | settings = { |
37 | 'template_path': path.join(path.dirname(__file__), 'templates'), | 37 | 'template_path': path.join(path.dirname(__file__), 'templates'), |
@@ -143,20 +143,36 @@ class TopicHandler(BaseHandler): | @@ -143,20 +143,36 @@ class TopicHandler(BaseHandler): | ||
143 | self.redirect('/') | 143 | self.redirect('/') |
144 | 144 | ||
145 | # ---------------------------------------------------------------------------- | 145 | # ---------------------------------------------------------------------------- |
146 | -# FIXME | 146 | +# Based on https://bhch.github.io/posts/2017/12/serving-large-files-with-tornado-safely-without-blocking/ |
147 | class FileHandler(BaseHandler): | 147 | class FileHandler(BaseHandler): |
148 | @tornado.web.authenticated | 148 | @tornado.web.authenticated |
149 | - def get(self, filename): | 149 | + async def get(self, filename): |
150 | uid = self.current_user | 150 | uid = self.current_user |
151 | public_dir = self.learn.get_current_public_dir(uid) | 151 | public_dir = self.learn.get_current_public_dir(uid) |
152 | filepath = path.expanduser(path.join(public_dir, filename)) | 152 | filepath = path.expanduser(path.join(public_dir, filename)) |
153 | + chunk_size = 1024 * 1024 # serve 1MiB multiple times | ||
153 | try: | 154 | try: |
154 | f = open(filepath, 'rb') | 155 | f = open(filepath, 'rb') |
155 | except FileNotFoundError: | 156 | except FileNotFoundError: |
156 | - raise tornado.web.HTTPError(404) | 157 | + logging.error(f'File not found: {filepath}') |
158 | + except PermissionError: | ||
159 | + logging.error(f'No permission: {filepath}') | ||
157 | else: | 160 | else: |
158 | - self.write(f.read()) | ||
159 | - f.close() | 161 | + with f: |
162 | + while True: | ||
163 | + chunk = f.read(chunk_size) | ||
164 | + if not chunk: break | ||
165 | + try: | ||
166 | + self.write(chunk) # write the cunk to response | ||
167 | + await self.flush() # flush the current chunk to socket | ||
168 | + except iostream.StreamClosedError: | ||
169 | + # client closed the connection | ||
170 | + break | ||
171 | + finally: | ||
172 | + del chunk | ||
173 | + await gen.sleep(0.000000001) # 1 nanosecond (hack) | ||
174 | + # in tornnado 5.0 use `await asyncio.sleep(0)` instead | ||
175 | + | ||
160 | 176 | ||
161 | # ---------------------------------------------------------------------------- | 177 | # ---------------------------------------------------------------------------- |
162 | # respond to AJAX to get a JSON question | 178 | # respond to AJAX to get a JSON question |
tools.py
@@ -99,19 +99,10 @@ class HighlightRenderer(mistune.Renderer): | @@ -99,19 +99,10 @@ class HighlightRenderer(mistune.Renderer): | ||
99 | def table(self, header, body): | 99 | def table(self, header, body): |
100 | return '<table class="table table-sm"><thead class="thead-light">' + header + '</thead><tbody>' + body + "</tbody></table>" | 100 | return '<table class="table table-sm"><thead class="thead-light">' + header + '</thead><tbody>' + body + "</tbody></table>" |
101 | 101 | ||
102 | - # def image(self, src, title, text): | ||
103 | - # if src.startswith('javascript:'): | ||
104 | - # src = '' | ||
105 | - # text = mistune.escape(text, quote=True) | ||
106 | - # if title: | ||
107 | - # title = mistune.escape(title, quote=True) | ||
108 | - # html = '<img class="img-responsive center-block" src="%s" alt="%s" title="%s"' % (src, text, title) | ||
109 | - # else: | ||
110 | - # html = '<img class="img-responsive center-block" src="%s" alt="%s"' % (src, text) | ||
111 | - # if self.options.get('use_xhtml'): | ||
112 | - # return '%s />' % html | ||
113 | - # return '%s>' % html | ||
114 | - | 102 | + def image(self, src, title, alt): |
103 | + alt = mistune.escape(alt, quote=True) | ||
104 | + title = mistune.escape(title or '', quote=True) | ||
105 | + return f'<img src="/file/{src}" class="img-fluid mx-auto d-block" alt="{alt}" title="{title}">' | ||
115 | 106 | ||
116 | # Pass math through unaltered - mathjax does the rendering in the browser | 107 | # Pass math through unaltered - mathjax does the rendering in the browser |
117 | def block_math(self, text): | 108 | def block_math(self, text): |