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.

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