You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

314 lines
9.7 KiB

3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
  1. # There're two ways to run this app:
  2. #
  3. # run the web
  4. # $ export FLASK_APP=app.py
  5. # $ flask run
  6. #
  7. # create or refresh the database
  8. # $ python3 app.py
  9. from flask import Flask, request, render_template, send_from_directory, abort, redirect, session, url_for, send_file
  10. from flask_sqlalchemy import SQLAlchemy
  11. from sqlalchemy import func
  12. from flask_limiter import Limiter
  13. from flask_limiter.util import get_remote_address
  14. import ipfshttpclient
  15. from mastodon import Mastodon
  16. import pypinyin
  17. from string import ascii_uppercase as AZ
  18. from datetime import date, datetime
  19. from functools import wraps
  20. import hashlib
  21. import shutil
  22. import random
  23. import os
  24. import re
  25. from config import C
  26. app = Flask(__name__)
  27. app.config.from_object('config.C')
  28. app.secret_key = C.session_key
  29. limiter = Limiter(
  30. app,
  31. key_func=get_remote_address,
  32. default_limits=["50 / minute"],
  33. )
  34. db = SQLAlchemy(app)
  35. ipfs_client = ipfshttpclient.connect()
  36. MAST_LOGIN_URL = Mastodon(api_base_url=C.mast_base_uri) \
  37. .auth_request_url(
  38. client_id = C.mast_client_id,
  39. redirect_uris = C.mast_redirect_uri,
  40. scopes = ['read:accounts']
  41. )
  42. class Paper(db.Model):
  43. id = db.Column(db.Integer, primary_key=True)
  44. course = db.Column(db.String(30), index=True)
  45. teacher = db.Column(db.String(30), index=True)
  46. year = db.Column(db.Integer, index=True)
  47. author = db.Column(db.String(30), index=True)
  48. notes = db.Column(db.String(200))
  49. anon = db.Column(db.Boolean)
  50. create_date = db.Column(db.Date)
  51. like_num = db.Column(db.Integer, index=True, default=0)
  52. down_num = db.Column(db.Integer, index=True, default=0)
  53. file_hash = db.Column(db.String(64))
  54. # always increment 1 for the id of a new record
  55. __table_args__ = { 'sqlite_autoincrement': True }
  56. def is_downloaded(self):
  57. return bool(DownloadRelation.query.filter_by(paper_id=self.id, username=session.get('username')).count())
  58. def is_liked(self):
  59. return bool(LikeRelation.query.filter_by(paper_id=self.id, username=session.get('username')).count())
  60. class DownloadRelation(db.Model):
  61. id = db.Column(db.Integer, primary_key=True)
  62. paper_id = db.Column(db.Integer, index=True)
  63. username = db.Column(db.String(30), index=True)
  64. class LikeRelation(db.Model):
  65. id = db.Column(db.Integer, primary_key=True)
  66. paper_id = db.Column(db.Integer, index=True)
  67. username = db.Column(db.String(30), index=True)
  68. if __name__ == "__main__":
  69. db.create_all()
  70. @app.route('/pastExam/img/<path:path>')
  71. def send_img(path):
  72. return send_from_directory('static/img', path)
  73. def login_required(allow_guest=True):
  74. def login_required_instance(f):
  75. @wraps(f)
  76. def df(*args, **kwargs):
  77. username = session.get('username')
  78. if not username or (not allow_guest and username.startswith('guest<')):
  79. return redirect(url_for('login'))
  80. return f(*args, **kwargs, username=username)
  81. return df
  82. return login_required_instance
  83. @app.route('/pastExam/login/')
  84. def login():
  85. return app.send_static_file('login/index.html')
  86. @app.route('/pastExam/logout')
  87. def logout():
  88. session.pop('username', None)
  89. return redirect('login')
  90. @app.route('/pastExam/login/guest/')
  91. def guest_login():
  92. return render_template('guest-login.html', vs=C.verify, allow_guest_upload=C.allow_guest_upload)
  93. @app.route('/pastExam/login/guest/verify', methods=['POST'])
  94. @limiter.limit("5 / hour")
  95. def guest_login_verify():
  96. for name, ques, hint, ans in C.verify:
  97. if request.form.get(name) != ans:
  98. return '错误!', 401
  99. if 'uid' not in session:
  100. session['uid'] = random.randint(0, 10000000)
  101. session['username'] = 'guest<%s>' % session['uid']
  102. session.pop('avatar', None)
  103. session.permanent = True
  104. return {'r':0}
  105. @app.route('/pastExam/login/mast/')
  106. def mast_login():
  107. return redirect(MAST_LOGIN_URL)
  108. @app.route('/pastExam/login/mast/auth')
  109. def mast_login_auth():
  110. code = request.args.get('code')
  111. client = Mastodon(
  112. client_id=C.mast_client_id,
  113. client_secret=C.mast_client_sec,
  114. api_base_url=C.mast_base_uri
  115. )
  116. token = client.log_in(code=code, redirect_uri=C.mast_redirect_uri,scopes=['read:accounts'])
  117. info = client.account_verify_credentials()
  118. session['username'] = info.acct
  119. session['avatar'] = info.avatar
  120. session.permanent = True
  121. return redirect(url_for('list'))
  122. def by_abc(l):
  123. d = {
  124. letter: [] for letter in AZ+'/'
  125. }
  126. for item in l:
  127. k = pypinyin.lazy_pinyin(item[0], style=pypinyin.Style.FIRST_LETTER, errors=lambda x:x[0])[0].upper()
  128. if k not in AZ:
  129. k = '/'
  130. d[k].append(item)
  131. return {k: v for k, v in d.items() if v}
  132. @app.route('/pastExam/')
  133. @login_required()
  134. def list(username):
  135. avatar = session.get('avatar', C.guest_avatar)
  136. course = request.args.get('course')
  137. teacher = request.args.get('teacher')
  138. year = request.args.get('year')
  139. year = year and year.isdigit() and int(year)
  140. is_my_upload = request.args.get('my_upload')
  141. is_my_fav = request.args.get('my_fav')
  142. has_course = course is not None
  143. has_teacher = teacher is not None
  144. has_year = year is not None
  145. ept = not (has_course or has_teacher or has_year) and is_my_fav is None and is_my_upload is None and 'page' not in request.args
  146. ps = Paper.query
  147. if course:
  148. ps = ps.filter_by(course=course)
  149. if teacher:
  150. ps = ps.filter_by(teacher=teacher)
  151. if year or year==0:
  152. ps = ps.filter_by(year=year)
  153. if is_my_upload:
  154. ps = ps.filter_by(author=username)
  155. if is_my_fav:
  156. ps = ps.join(LikeRelation, Paper.id==LikeRelation.paper_id).filter(LikeRelation.username==username)
  157. ps = ps.order_by(db.desc('like_num'))
  158. pagination = ps.paginate(max_per_page=100)
  159. curr_year = date.today().year
  160. all_courses = db.session.query(Paper.course, func.count()).group_by(Paper.course).all()
  161. all_courses_abc = by_abc(all_courses)
  162. all_teachers = db.session.query(Paper.teacher, func.count()).group_by(Paper.teacher).all()
  163. all_years = db.session.query(Paper.year, func.count()).group_by(Paper.year).all()
  164. ipfs_version = hashlib.sha256(C.ipfs_base_url.encode('utf-8')).hexdigest()
  165. disable_upload = not C.allow_guest_upload and username.startswith('guest<')
  166. return render_template('list.html', **locals())
  167. def check_length(x, limit=30, allow_null=False):
  168. return (x and len(x) <= limit) or (allow_null and not x)
  169. @app.route('/pastExam/upload', methods=['POST'])
  170. @limiter.limit("10 / hour")
  171. @login_required(allow_guest=C.allow_guest_upload)
  172. def upload(username):
  173. name = request.form.get('name')
  174. teacher = request.form.get('teacher')
  175. year = request.form.get('year')
  176. year = year and year.isdigit() and int(year) or 0
  177. notes = request.form.get('notes', '')
  178. anon = request.form.get('anon') == 'on'
  179. if not (check_length(name) and check_length(teacher) and check_length(notes, 200, True)):
  180. abort(422)
  181. files = request.files.getlist('files[]')
  182. print(files)
  183. dir_name = username + str(datetime.now())
  184. base_path = os.path.join('/tmp', dir_name)
  185. os.mkdir(base_path)
  186. for f in files:
  187. filename = f.filename.replace('..','__')
  188. os.makedirs(os.path.dirname(os.path.join(base_path, filename)), exist_ok=True)
  189. f.save(os.path.join(base_path, filename))
  190. res = ipfs_client.add(base_path, recursive=True)
  191. file_hash = ''
  192. for r in res:
  193. if r.get('Name') == dir_name:
  194. file_hash = r.get('Hash')
  195. if not file_hash:
  196. abort(500)
  197. paper = Paper(
  198. course=name,
  199. teacher=teacher,
  200. year=year,
  201. notes=notes,
  202. anon=anon,
  203. author=username,
  204. create_date=date.today(),
  205. file_hash=file_hash
  206. )
  207. db.session.add(paper)
  208. db.session.commit()
  209. return redirect('.#part2')
  210. @app.route('/pastExam/<pid>/download')
  211. @limiter.limit("100 / hour")
  212. @login_required()
  213. def download(pid, username):
  214. p = Paper.query.get_or_404(pid)
  215. r = DownloadRelation.query.filter_by(paper_id=pid, username=username).count()
  216. if not r:
  217. r = DownloadRelation(paper_id=pid, username=username)
  218. db.session.add(r)
  219. p.down_num += 1
  220. db.session.commit()
  221. dformat = request.args.get('format')
  222. suff = {
  223. 'zip': '.zip',
  224. 'tar': '.tar',
  225. 'gztar': '.tar.gz'
  226. }
  227. if dformat in suff:
  228. target_file = '/tmp/%s' % pid
  229. target_filename = target_file + suff[dformat]
  230. target_dir = os.path.join('/tmp', p.file_hash)
  231. if not os.path.exists(target_filename):
  232. if not os.path.exists(target_dir):
  233. ipfs_client.get(p.file_hash, target='/tmp')
  234. shutil.make_archive(target_file, dformat, target_dir)
  235. filename = re.sub('[^\w@_()()-]', '_', '%s_%s_共享计划_%d' %(p.course, p.teacher, p.id)) + suff[dformat]
  236. return send_file(target_filename, as_attachment=True, attachment_filename=filename)
  237. return redirect(C.ipfs_base_url + p.file_hash, code=301) # 301减少不必要的请求
  238. @app.route('/pastExam/<pid>/like', methods=['POST', 'DELETE'])
  239. @limiter.limit("100 / hour")
  240. @login_required()
  241. def like(pid, username):
  242. p = Paper.query.get_or_404(pid)
  243. r = LikeRelation.query.filter_by(paper_id=pid, username=username).first()
  244. if request.method == 'POST':
  245. if not r:
  246. r = LikeRelation(paper_id=p.id, username=username)
  247. db.session.add(r)
  248. p.like_num += 1
  249. db.session.commit()
  250. else:
  251. if r:
  252. db.session.delete(r)
  253. p.like_num -= 1
  254. db.session.commit()
  255. return str(p.like_num)