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 | 11 | |
12 | 12 | # TODO |
13 | 13 | |
14 | -- servir imagens/ficheiros. | |
15 | 14 | - session management. close after inactive time. |
16 | 15 | - each topic only loads a sample of K questions (max) in random order. |
17 | 16 | - radio e checkboxes, aceitar numeros como seleccao das opcoes. |
... | ... | @@ -30,6 +29,7 @@ |
30 | 29 | |
31 | 30 | # FIXED |
32 | 31 | |
32 | +- servir imagens/ficheiros. | |
33 | 33 | - radio: suporte para multiplas opcoes correctas e erradas, escolhendo-se uma selecção aleatoria destas (so com 1 certa). |
34 | 34 | - checkbox: cada opção pode ser uma dupla (certo, errado) sendo escolhida uma aleatória. |
35 | 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 | 30 | (r'/change_password', ChangePasswordHandler), |
31 | 31 | (r'/question', QuestionHandler), # each question |
32 | 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 | 36 | settings = { |
37 | 37 | 'template_path': path.join(path.dirname(__file__), 'templates'), |
... | ... | @@ -143,20 +143,36 @@ class TopicHandler(BaseHandler): |
143 | 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 | 147 | class FileHandler(BaseHandler): |
148 | 148 | @tornado.web.authenticated |
149 | - def get(self, filename): | |
149 | + async def get(self, filename): | |
150 | 150 | uid = self.current_user |
151 | 151 | public_dir = self.learn.get_current_public_dir(uid) |
152 | 152 | filepath = path.expanduser(path.join(public_dir, filename)) |
153 | + chunk_size = 1024 * 1024 # serve 1MiB multiple times | |
153 | 154 | try: |
154 | 155 | f = open(filepath, 'rb') |
155 | 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 | 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 | 178 | # respond to AJAX to get a JSON question | ... | ... |
tools.py
... | ... | @@ -99,19 +99,10 @@ class HighlightRenderer(mistune.Renderer): |
99 | 99 | def table(self, header, body): |
100 | 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 | 107 | # Pass math through unaltered - mathjax does the rendering in the browser |
117 | 108 | def block_math(self, text): | ... | ... |