diff --git a/README.md b/README.md index 630c091..0d15a3d 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ python manage.py db init **本地运行** -``` +```bash python manage.py runserver -h0.0.0.0 -p80 ``` @@ -40,11 +40,17 @@ export flask_server_type="development" 启动服务,推荐使用[Gunicorn](http://gunicorn.org/),步骤如下: +```bash +gunicorn -w 3 manage:app -b 0.0.0.0:8000 ``` -gunicorn -w 3 manage:app -b 0.0.0.0:80 -``` -由于暂时基本没有静态文件的访问需求,故没有使用Nginx服务器。 +第三步: + +配置nginx,转发80端口请求到gunicorn: + +```bash +sudo ln -s deployment/nginx /etc/nginx/sites-enabled/mysitename.conf +``` API设计及文档 ------- diff --git a/app/__init__.py b/app/__init__.py index 408260d..ad5fe63 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -18,6 +18,7 @@ login_manager.login_view = 'api.login' rank = Rank() + def create_app(config_name): app = Flask(__name__) app.config.from_object(config[config_name]) @@ -34,7 +35,7 @@ def create_app(config_name): rd.init_app(app) login_manager.init_app(app) task.init_app(app) - scheduler.init_app(app) # access scheduler from app.scheduler + scheduler.init_app(app) # access scheduler from app.scheduler """ Running in gunicorn: @@ -43,8 +44,8 @@ def create_app(config_name): Read: https://stackoverflow.com/questions/16053364 """ if not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true': - # prevents scheduler start twice (only in debug mode) - # https://stackoverflow.com/questions/14874782 + # prevents scheduler start twice (only in debug mode) + # https://stackoverflow.com/questions/14874782 scheduler.start() add_init_jobs() atexit.register(lambda: scheduler.shutdown()) diff --git a/app/api/aliyun_mail.py b/app/api/aliyun_mail.py index 1c3f153..208a642 100644 --- a/app/api/aliyun_mail.py +++ b/app/api/aliyun_mail.py @@ -6,6 +6,8 @@ import uuid import requests + + ActivationSubject = "欢迎注册,请验证您的邮箱" ActivationText = """尊敬的用户,您好! diff --git a/config.py b/config.py index 3525955..6388d4d 100644 --- a/config.py +++ b/config.py @@ -6,19 +6,19 @@ class Config: DEBUG = True - ### For sqlalchemy + # For sqlalchemy SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'data-test.sqlite') SQLALCHEMY_TRACK_MODIFICATIONS = False SECRET_KEY = '123' TRAP_BAD_REQUEST_ERRORS = False # For restful api TRAP_HTTP_EXCEPTIONS = False - ### For Flask-Redis + # For Flask-Redis REDIS_URL = 'redis://localhost:6379/0' - ### For Flask-RQ2 + # For Flask-RQ2 RQ_REDIS_URL = REDIS_URL - ### For APScheduler + # For APScheduler SCHEDULER_API_ENABLED = True SCHEDULER_API_PREFIX = "/t" SCHEDULER_JOBSTORES = { @@ -32,8 +32,6 @@ class Config: @staticmethod def init_app(app): import logging - from logging import StreamHandler - #file_handler = StreamHandler() file_handler = logging.FileHandler("app.log") file_handler.setLevel(logging.DEBUG) app.logger.addHandler(file_handler) diff --git a/deployment/nginx.conf b/deployment/nginx.conf index d34e2e8..db9d5e5 100644 --- a/deployment/nginx.conf +++ b/deployment/nginx.conf @@ -22,7 +22,7 @@ server { listen 80; # server_name server_domain_or_ip # Note:if there is only one server block, server name - # can be ommited, and all request will be process by + # can be omitted, and all request will be process by # this only server server_name 47.93.240.135; diff --git a/scripts/db_auto_backup.py b/scripts/db_auto_backup.py new file mode 100644 index 0000000..48cf020 --- /dev/null +++ b/scripts/db_auto_backup.py @@ -0,0 +1,116 @@ +# -*- coding: UTF-8 -*- + + +"""本脚本用于定时备份mysql数据库 + +将mysqldump导出的数据存储到又拍云上。 + +每晚凌晨3点备份数据,Crontab命令如下: +0 3 * * * /bin/bash/python3 /path/to/file.py +""" + + +import hmac +import base64 +import hashlib +import logging +import subprocess +from datetime import datetime +from requests import Session, Request + +logging.basicConfig(filename="db_auto_backup.log", level=logging.DEBUG) + + +# 设置又拍云信息 +# 注意: secret = hashlib.md5('操作员密码'.encode()).hexdigest() +OPERATOR_NAME = 'autobackup' +OPERATOR_SECRET = '51bfdb1f889e96484bf31eb74a82e804' +SERVICE_NAME = 'school-db-autobackup' +SERVER_URL = 'https://v0.api.upyun.com' + +DB_USER = 'test' +DB_PASSWORD = 'Yq!((&1024' +DB_DATABASE = 'TEST' + +class Upyun: + """使用又拍云提供的REST API上传文件 + + 文档地址:https://help.upyun.com/knowledge-base/rest_api/ + 认证方式采用签名认证:https://help.upyun.com/knowledge-base/object_storage_authorization/#e7adbee5908de8aea4e8af81 + """ + + @staticmethod + def _httpdate_rfc1123(dt=None): + dt = dt or datetime.utcnow() + return dt.strftime('%a, %d %b %Y %H:%M:%S GMT') + + @staticmethod + def _sign(client_key, client_secret, method, uri, date, policy=None, md5=None): + """签名""" + signarr = [] + for v in [method, uri, date, policy, md5]: + if v is not None: + signarr.append(v) + signstr = '&'.join(signarr) + signstr = base64.b64encode( + hmac.new(client_secret.encode(), signstr.encode(), + digestmod=hashlib.sha1).digest() + ).decode() + return 'UPYUN %s:%s' % (client_key, signstr) + + @staticmethod + def _send_request(method, url, headers, body): + """发送请求""" + s = Session() + prepped = Request(method, url).prepare() + prepped.headers = headers + prepped.body = body + r = s.send(prepped) + return r.status_code, r.text + + @staticmethod + def upload(file_path, target_name, expire=None): + """上传文件 + + :param file_path: 本地路径 + :param target_name: 又拍云上的存储路径 + :param expire: 过期时间(单位:天), 可选 + :return: None + """ + with open(file_path, 'rb') as f: + file = f.read() + date = Upyun._httpdate_rfc1123() + file_md5 = hashlib.md5(file).hexdigest() + uri = '/{service}/{target}'.format(service=SERVICE_NAME, target=target_name) + method = 'PUT' + signature = Upyun._sign(OPERATOR_NAME, OPERATOR_SECRET, method, uri, date, md5=file_md5) + print(signature) + headers = { + 'Authorization': signature, + 'Date': date, + 'Content-Length': str(len(file)), + 'Content-MD5': file_md5, + } + if expire is not None: + headers['x-upyun-meta-ttl'] = expire + + status, content = Upyun._send_request(method, SERVER_URL + uri, headers, file) + + if status != 200: + raise Exception("Can't upload file via API, status=%d, response: %s" % (status, content)) + + +def main(): + backup_file_name = datetime.now().strftime('autobackup-%Y-%m-%d-%H:%M:%S.mysql-db.gz') + cmd = "mysqldump -u{user} -p{password} {db_name} | gzip > {output}".format( + user=DB_USER, password=DB_PASSWORD, db_name=DB_DATABASE, output=backup_file_name) + try: + subprocess.run(cmd, check=True) + Upyun.upload(backup_file_name, backup_file_name, 30) + except: + logging.exception("备份失败!") + + +if __name__ == '__main__': + main() + exit(0) diff --git a/test/upyun.py b/test/upyun.py index a67a892..5398122 100644 --- a/test/upyun.py +++ b/test/upyun.py @@ -9,7 +9,7 @@ # 使用时需要根据自己配置与需求提供参数 key secret uri method key = 'fondoger' secret = '97eef486892c0b66f08a3228ebf676a3' -#secret = hashlib.md5('woshiwang2'.encode()).hexdigest() +#secret = hashlib.md5('操作员密码'.encode()).hexdigest() def httpdate_rfc1123(dt=None): diff --git a/users_event.py b/users_event.py deleted file mode 100644 index a5150b3..0000000 --- a/users_event.py +++ /dev/null @@ -1,7 +0,0 @@ -from sqlalchemy import event -from . import * - -@event.listen_for(User.followers, "init_collection") -def my_listener(*args): - print(args) -