├── object
├── __init__.py
├── comment.py
├── msg.py
├── archive.py
├── blog.py
└── user.py
├── static
└── styles
│ ├── auth
│ ├── yours.css
│ ├── login.css
│ ├── passwd.css
│ ├── register.css
│ ├── role.css
│ └── delete.css
│ ├── oss
│ └── upload.css
│ ├── docx
│ ├── docx.css
│ └── article.css
│ ├── msg
│ └── msg.css
│ ├── base.css
│ ├── archive
│ └── archive.css
│ └── index
│ └── hello.css
├── .gitmodules
├── templates
├── email-msg
│ ├── register.txt
│ └── register.html
├── about_me
│ └── about_me.html
├── error.html
├── auth
│ ├── passwd.html
│ ├── login.html
│ ├── register.html
│ ├── delete.html
│ ├── yours.html
│ └── role.html
├── oss
│ └── upload.html
├── macro.html
├── index
│ ├── index.html
│ └── hello.html
├── msg
│ └── msg.html
├── archive
│ └── archive.html
├── docx
│ ├── docx.html
│ └── article.html
└── base.html
├── app
├── __init__.py
├── cache.py
├── http_auth.py
├── about_me.py
├── tool.py
├── index.py
├── oss.py
├── archive.py
├── msg.py
├── app.py
├── api.py
├── auth.py
└── docx.py
├── Dockerfile
├── send_email
└── __init__.py
├── sql
├── redis.py
├── __init__.py
├── statistics.py
├── base.py
├── cache_refresh.py
├── comment.py
├── archive.py
├── msg.py
├── mysql.py
├── user.py
├── blog.py
└── cache.py
├── requirements.txt
├── main.py
├── aliyun
└── __init__.py
├── gunicorn.conf.py
├── README.md
├── .dockerignore
├── .gitignore
├── configure
└── __init__.py
└── init.sql
/object/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/styles/auth/yours.css:
--------------------------------------------------------------------------------
1 | .modal div {
2 | background-color: white;
3 | border-radius: 10px;
4 | }
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "static/editor.md"]
2 | path = static/editor.md
3 | url = https://github.com/pandao/editor.md.git
4 |
--------------------------------------------------------------------------------
/templates/email-msg/register.txt:
--------------------------------------------------------------------------------
1 | 欢迎注册 {{ conf["BLOG_NAME"] }} 用户,请点击以下链接完成认证。
2 | 若您未进行过注册操作,请忽略此邮件。
3 | {{ register_url }}
4 |
--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
1 | from .tool import role_required, form_required
2 | from .app import HBlogFlask, Hblog
3 | from .cache import cache
4 |
--------------------------------------------------------------------------------
/static/styles/auth/login.css:
--------------------------------------------------------------------------------
1 | .login-form {
2 | background-color: white;
3 | border-radius: 10px;
4 | border: 2px solid #6b6882;
5 | padding: 15px;
6 | }
--------------------------------------------------------------------------------
/static/styles/oss/upload.css:
--------------------------------------------------------------------------------
1 | .upload-form {
2 | background-color: white;
3 | border-radius: 10px;
4 | border: 2px solid #6b6882;
5 | padding: 15px;
6 | }
--------------------------------------------------------------------------------
/static/styles/auth/passwd.css:
--------------------------------------------------------------------------------
1 | .passwd-form {
2 | background-color: white;
3 | border-radius: 10px;
4 | border: 2px solid #6b6882;
5 | padding: 15px;
6 | }
--------------------------------------------------------------------------------
/static/styles/auth/register.css:
--------------------------------------------------------------------------------
1 | .register-form {
2 | background-color: white;
3 | border-radius: 10px;
4 | border: 2px solid #6b6882;
5 | padding: 15px;
6 | }
--------------------------------------------------------------------------------
/static/styles/auth/role.css:
--------------------------------------------------------------------------------
1 | .role-form {
2 | background-color: white;
3 | border-radius: 10px;
4 | border: 2px solid #6b6882;
5 | padding: 15px;
6 | }
7 |
--------------------------------------------------------------------------------
/static/styles/docx/docx.css:
--------------------------------------------------------------------------------
1 | .markdown {
2 | background-color: white;
3 | padding: 15px;
4 | border: 2px solid #0074D9;
5 | border-radius: 10px;
6 | }
7 |
8 | .modal div {
9 | background-color: white;
10 | border-radius: 10px;
11 | }
--------------------------------------------------------------------------------
/static/styles/auth/delete.css:
--------------------------------------------------------------------------------
1 | .delete-form {
2 | background-color: white;
3 | border-radius: 10px;
4 | border: 2px solid #6b6882;
5 | padding: 15px;
6 | }
7 |
8 | .modal div {
9 | background-color: white;
10 | border-radius: 10px;
11 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.9-bullseye
2 | WORKDIR /app
3 | COPY . .
4 | RUN ["python", "-m", "pip", "install", "--upgrade", "pip"]
5 | RUN ["python", "-m", "pip", "install", "-r", "requirements.txt"]
6 | RUN ["python", "-m", "pip", "install", "gunicorn"]
7 | EXPOSE 80
8 | ENTRYPOINT ["python", "-m", "gunicorn", "-c", "./gunicorn.conf.py", "main:app"]
--------------------------------------------------------------------------------
/static/styles/msg/msg.css:
--------------------------------------------------------------------------------
1 | .msg {
2 | margin-bottom: 20px;
3 | border-radius: 10px;
4 | border: 2px solid #FFDC00;
5 | padding: 15px;
6 | background-color: white;
7 | }
8 |
9 | .msg .msg-title {
10 | color: black;
11 | text-decoration: none;
12 | }
13 |
14 | .modal div {
15 | background-color: white;
16 | border-radius: 10px;
17 | }
--------------------------------------------------------------------------------
/static/styles/base.css:
--------------------------------------------------------------------------------
1 | html {
2 | word-break: break-word;
3 | }
4 |
5 | body {
6 | overflow-y: scroll;
7 | overflow-x: hidden;
8 | }
9 |
10 | #foot {
11 | text-align: center;
12 | padding-bottom: 10px;
13 | }
14 |
15 | #ICP {
16 | background-color: rgb(255, 255, 255, 60%);
17 | }
18 |
19 | #GONG_AN {
20 | background-color: rgb(255, 255, 255, 60%);
21 | }
--------------------------------------------------------------------------------
/static/styles/docx/article.css:
--------------------------------------------------------------------------------
1 | .comment {
2 | margin-bottom: 20px;
3 | border-radius: 10px;
4 | border: 2px solid #FFDC00;
5 | padding: 15px;
6 | background-color: white;
7 | }
8 |
9 | .comment .comment-title {
10 | color: black;
11 | text-decoration: none;
12 | }
13 |
14 | .modal div {
15 | background-color: white;
16 | border-radius: 10px;
17 | }
--------------------------------------------------------------------------------
/app/cache.py:
--------------------------------------------------------------------------------
1 | from flask_caching import Cache
2 | from configure import conf
3 |
4 | cache = Cache(config={
5 | 'CACHE_TYPE': 'RedisCache',
6 | 'CACHE_KEY_PREFIX': f'{conf["FLASK_CACHE_PREFIX"]}:',
7 | 'CACHE_REDIS_URL': f'redis://{conf["CACHE_REDIS_NAME"]}:{conf["CACHE_REDIS_PASSWD"]}@'
8 | f'{conf["CACHE_REDIS_HOST"]}:{conf["CACHE_REDIS_PORT"]}/{conf["CACHE_REDIS_DATABASE"]}'
9 | })
10 |
--------------------------------------------------------------------------------
/static/styles/archive/archive.css:
--------------------------------------------------------------------------------
1 | .archive {
2 | background-color: white;
3 | border-radius: 10px;
4 | border: 2px solid #39CCCC;
5 | min-width: 30%;
6 | min-height: 20%;
7 | padding: 15px;
8 | }
9 |
10 | .create {
11 | background-color: white;
12 | border-radius: 10px;
13 | border: 2px solid #1685a9;
14 | padding: 15px;
15 | }
16 |
17 | .modal div {
18 | background-color: white;
19 | border-radius: 10px;
20 | }
21 |
22 | .archive_describe {
23 | min-height: 2em;
24 | }
--------------------------------------------------------------------------------
/app/http_auth.py:
--------------------------------------------------------------------------------
1 | from flask import jsonify, g
2 | from flask_httpauth import HTTPBasicAuth
3 | from object.user import User
4 |
5 |
6 | http_auth = HTTPBasicAuth()
7 |
8 |
9 | @http_auth.verify_password
10 | def verify_passwd(email, passwd):
11 | user = User(email)
12 | g.user = user
13 | return user.check_passwd(passwd)
14 |
15 |
16 | @http_auth.error_handler
17 | def unauthorized():
18 | rsp = jsonify({"status": 403, 'error': 'Unauthorized access'})
19 | rsp.status_code = 403
20 | return rsp
21 |
--------------------------------------------------------------------------------
/templates/about_me/about_me.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %} 关于我 {% endblock %}
4 |
5 | {% block style %}
6 | {{ super() }}
7 | {% endblock %}
8 |
9 | {% block content %}
10 | {% cache conf["CACHE_EXPIRE"], ":about_me" %}
11 |
12 |
13 |
14 | {{ about_me | safe }}
15 |
16 |
17 |
18 | {% endcache %}
19 | {% endblock %}
--------------------------------------------------------------------------------
/app/about_me.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, render_template, current_app
2 | import app
3 | from configure import conf
4 |
5 | about_me = Blueprint("about_me", __name__)
6 |
7 |
8 | @about_me.route('/')
9 | def about_me_page():
10 | app.HBlogFlask.print_load_page_log("about me")
11 | hblog: app.Hblog = current_app
12 | return render_template("about_me/about_me.html", about_me=hblog.about_me)
13 |
14 |
15 | @about_me.context_processor
16 | @app.cache.cached(timeout=conf["CACHE_EXPIRE"], key_prefix="inject_base:about_me")
17 | def inject_base():
18 | """ about me 默认模板变量 """
19 | return {"top_nav": ["", "", "", "", "active", ""]}
20 |
--------------------------------------------------------------------------------
/templates/error.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %} 错误 {% endblock %}
4 |
5 | {% block style %}
6 | {{ super() }}
7 |
8 | {% endblock %}
9 |
10 | {% block content %}
11 |
12 |
13 |
14 |
{{ error_info }}
15 |
回到主页
16 |
17 |
18 |
19 | {% endblock %}
--------------------------------------------------------------------------------
/send_email/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import render_template, current_app
2 | from flask_mail import Mail, Message
3 | from configure import conf
4 |
5 |
6 | def send_msg(title: str, mail: Mail, to, template, **kwargs):
7 | """ 邮件发送 """
8 | sender = conf['MAIL_SENDER']
9 | message = Message(conf['MAIL_PREFIX'] + title, sender=sender, recipients=[to])
10 | message.body = render_template("email-msg/" + template + ".txt", **kwargs)
11 | message.html = render_template("email-msg/" + template + ".html", **kwargs)
12 | mail.send(message)
13 | current_app.logger.info(f"Send email to {to} sender: {sender} msg: {template} kwargs: {kwargs}")
14 |
--------------------------------------------------------------------------------
/sql/redis.py:
--------------------------------------------------------------------------------
1 | import redis
2 | import logging
3 | import logging.handlers
4 | from configure import conf
5 | import os
6 |
7 |
8 | class RedisDB(redis.StrictRedis):
9 | def __init__(self, host, port, username, passwd, db):
10 | super().__init__(host=host, port=port, username=username, password=passwd, db=db, decode_responses=True)
11 |
12 | # redis是线程安全的
13 |
14 | self.logger = logging.getLogger("main.database")
15 | self.logger.setLevel(conf["LOG_LEVEL"])
16 | if len(conf["LOG_HOME"]) > 0:
17 | handle = logging.handlers.TimedRotatingFileHandler(
18 | os.path.join(conf["LOG_HOME"], f"redis-{username}@{host}.log"), backupCount=10)
19 | handle.setFormatter(logging.Formatter(conf["LOG_FORMAT"]))
20 | self.logger.addHandler(handle)
21 |
--------------------------------------------------------------------------------
/templates/email-msg/register.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 | {{ title }}
18 |
19 |
20 |
21 |
22 | {{ title }}
23 | 欢迎注册 {{ conf["BLOG_NAME"] }} 用户,请点击以下链接完成认证。
若您未进行过注册操作,请忽略此邮件。
24 | 点击完成认证
25 |
26 |
27 |
--------------------------------------------------------------------------------
/sql/__init__.py:
--------------------------------------------------------------------------------
1 | from sql.mysql import MysqlDB
2 | from sql.redis import RedisDB
3 | from configure import conf
4 |
5 | DB = MysqlDB
6 | db = DB(host=conf["MYSQL_URL"],
7 | name=conf["MYSQL_NAME"],
8 | passwd=conf["MYSQL_PASSWD"],
9 | port=conf["MYSQL_PORT"],
10 | database=conf["MYSQL_DATABASE"])
11 |
12 | cache = redis.RedisDB(host=conf["CACHE_REDIS_HOST"],
13 | port=conf["CACHE_REDIS_PORT"],
14 | username=conf["CACHE_REDIS_NAME"],
15 | passwd=conf["CACHE_REDIS_PASSWD"],
16 | db=conf["CACHE_REDIS_DATABASE"])
17 |
18 | redis = redis.RedisDB(host=conf["REDIS_HOST"],
19 | port=conf["REDIS_PORT"],
20 | username=conf["REDIS_NAME"],
21 | passwd=conf["REDIS_PASSWD"],
22 | db=conf["REDIS_DATABASE"])
23 |
--------------------------------------------------------------------------------
/sql/statistics.py:
--------------------------------------------------------------------------------
1 | from sql import redis
2 | from configure import conf
3 |
4 |
5 | PREFIX = conf["REDIS_PREFIX"]
6 |
7 |
8 | def add_hello_click():
9 | redis.incr(f"{PREFIX}:home", amount=1)
10 |
11 |
12 | def get_hello_click():
13 | res = redis.get(f"{PREFIX}:home")
14 | return res if res else 0
15 |
16 |
17 | def add_home_click():
18 | redis.incr(f"{PREFIX}:index", amount=1)
19 |
20 |
21 | def get_home_click():
22 | res = redis.get(f"{PREFIX}:index")
23 | return res if res else 0
24 |
25 |
26 | def add_blog_click(blog_id: int):
27 | redis.incr(f"{PREFIX}:blog:{blog_id}", amount=1)
28 |
29 |
30 | def get_blog_click(blog_id: int):
31 | res = redis.get(f"{PREFIX}:blog:{blog_id}")
32 | return res if res else 0
33 |
34 |
35 | def add_archive_click(archive_id: int):
36 | redis.incr(f"{PREFIX}:archive:{archive_id}", amount=1)
37 |
38 |
39 | def get_archive_click(archive_id: int):
40 | res = redis.get(f"{PREFIX}:archive:{archive_id}")
41 | return res if res else 0
42 |
--------------------------------------------------------------------------------
/templates/auth/passwd.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %} 修改密码 {% endblock %}
4 |
5 | {% block style %}
6 | {{ super() }}
7 |
8 | {% endblock %}
9 |
10 | {% block content %}
11 |
27 | {% endblock %}
--------------------------------------------------------------------------------
/templates/auth/login.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %} 登录 {% endblock %}
4 |
5 | {% block style %}
6 | {{ super() }}
7 |
8 | {% endblock %}
9 |
10 | {% block content %}
11 |
27 | {% endblock %}
--------------------------------------------------------------------------------
/templates/auth/register.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %} 注册 {% endblock %}
4 |
5 | {% block style %}
6 | {{ super() }}
7 |
8 | {% endblock %}
9 |
10 | {% block content %}
11 |
27 | {% endblock %}
--------------------------------------------------------------------------------
/templates/oss/upload.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %} 上传文件 {% endblock %}
4 |
5 | {% block style %}
6 | {{ super() }}
7 |
8 | {% endblock %}
9 |
10 | {% block content %}
11 |
32 | {% endblock %}
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | aliyun-python-sdk-core==2.13.35
2 | aliyun-python-sdk-kms==2.15.0
3 | async-timeout==4.0.2
4 | beautifulsoup4==4.10.0
5 | blinker==1.4
6 | bs4==0.0.1
7 | cachelib==0.9.0
8 | certifi==2021.10.8
9 | cffi==1.15.0
10 | charset-normalizer==2.0.8
11 | click==8.0.3
12 | colorama==0.4.4
13 | crcmod==1.7
14 | cryptography==36.0.0
15 | DBUtils==3.0.2
16 | Deprecated==1.2.13
17 | dnspython==2.1.0
18 | email-validator==1.1.3
19 | Flask==2.0.2
20 | Flask-Caching==2.0.1
21 | Flask-HTTPAuth==4.7.0
22 | Flask-Login==0.5.0
23 | Flask-Mail==0.9.1
24 | Flask-Moment==1.0.5
25 | Flask-WTF==1.0.0
26 | graphviz==0.19.1
27 | greenlet==1.1.2
28 | gunicorn==20.1.0
29 | idna==3.3
30 | importlib-metadata==4.8.2
31 | itsdangerous==2.0.1
32 | Jinja2==3.0.3
33 | jmespath==0.10.0
34 | MarkupSafe==2.0.1
35 | ndg-httpsclient==0.5.1
36 | objgraph==3.5.0
37 | oss2==2.15.0
38 | packaging==21.3
39 | pyasn1==0.4.8
40 | pycparser==2.21
41 | pycryptodome==3.11.0
42 | Pympler==1.0.1
43 | PyMySQL==1.0.2
44 | pyOpenSSL==22.0.0
45 | pyparsing==3.0.6
46 | redis==4.3.4
47 | requests==2.26.0
48 | six==1.16.0
49 | soupsieve==2.3.1
50 | urllib3==1.26.7
51 | waitress==2.0.0
52 | webencodings==0.5.1
53 | Werkzeug==2.0.2
54 | wrapt==1.14.1
55 | WTForms==3.0.0
56 | zipp==3.6.0
57 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | from configure import configure, conf
2 |
3 | import os
4 | import logging
5 | import threading
6 |
7 | env_dict = os.environ
8 | hblog_conf = env_dict.get("HBLOG_CONF")
9 | if hblog_conf is None:
10 | logging.info("Configure file ./etc/conf.json")
11 | configure("./etc/conf.json")
12 | else:
13 | logging.info(f"Configure file {hblog_conf}")
14 | configure(hblog_conf)
15 |
16 | from app import HBlogFlask
17 | from waitress import serve
18 |
19 | app = HBlogFlask(__name__)
20 | app.register_all_blueprint()
21 |
22 | from sql.cache import restart_clear_cache
23 | from sql.cache_refresh import refresh
24 | restart_clear_cache() # 清理缓存
25 |
26 |
27 | @app.before_first_request
28 | def before_first_requests():
29 | class FirstRefresh(threading.Thread):
30 | def __init__(self):
31 | super(FirstRefresh, self).__init__()
32 | self.daemon = True # 设置为守护进程
33 |
34 | def run(self):
35 | refresh()
36 |
37 | class TimerRefresh(threading.Timer):
38 | def __init__(self):
39 | super(TimerRefresh, self).__init__(conf["CACHE_REFRESH_INTERVAL"], refresh)
40 | self.daemon = True # 设置为守护进程
41 |
42 | FirstRefresh().start()
43 | TimerRefresh().start()
44 |
45 |
46 | if __name__ == '__main__':
47 | logging.info("Server start on 127.0.0.1:8080")
48 | serve(app, host='0.0.0.0', port="8080")
49 |
--------------------------------------------------------------------------------
/templates/macro.html:
--------------------------------------------------------------------------------
1 | {% macro get_page_list(info_lines, now_page) %}
2 | {% for line in info_lines %}
3 | {% if line %}
4 | {% if now_page == line[0] %}
5 | {{ line[0] }}
6 | {% else %}
7 | {{ line[0] }}
8 | {% endif %}
9 | {% else %}
10 | ...
11 | {% endif %}
12 | {% endfor %}
13 | {% endmacro %}
14 |
15 | {% macro render_field(field) %}
16 |
28 | {% endmacro %}
29 |
30 | {% macro render_select_field(field) %}
31 |
37 | {% endmacro %}
--------------------------------------------------------------------------------
/app/tool.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 | from flask import abort, g, redirect, url_for
3 | from flask_login import current_user
4 | from flask_wtf import FlaskForm
5 | from typing import ClassVar, Optional, Callable
6 | import app
7 |
8 |
9 | def role_required(role: str, opt: str):
10 | def required(func):
11 | @wraps(func)
12 | def new_func(*args, **kwargs):
13 | if not current_user.check_role(role): # 检查相应的权限
14 | app.HBlogFlask.print_user_not_allow_opt_log(opt)
15 | return abort(403)
16 | return func(*args, **kwargs)
17 | return new_func
18 | return required
19 |
20 |
21 |
22 | def api_role_required(role: str, opt: str):
23 | def required(func):
24 | @wraps(func)
25 | def new_func(*args, **kwargs):
26 | if not g.user.check_role(role): # 检查相应的权限
27 | app.HBlogFlask.print_user_not_allow_opt_log(opt)
28 | return abort(403)
29 | return func(*args, **kwargs)
30 | return new_func
31 | return required
32 |
33 |
34 | def form_required(form: ClassVar[FlaskForm], opt: str, callback: Optional[Callable] = None, **kw):
35 | def required(func):
36 | @wraps(func)
37 | def new_func(*args, **kwargs):
38 | f = form()
39 | if not f.validate_on_submit():
40 | app.HBlogFlask.print_form_error_log(opt)
41 | if callback is None:
42 | return abort(404)
43 | return callback(form=f, **kw, **kwargs)
44 | g.form = f
45 | return func(*args, **kwargs)
46 | return new_func
47 | return required
48 |
--------------------------------------------------------------------------------
/object/comment.py:
--------------------------------------------------------------------------------
1 | from collections import namedtuple
2 | from datetime import datetime
3 |
4 | from sql.comment import read_comment_list, create_comment, get_user_comment_count, delete_comment, read_comment
5 | import object.user
6 | import object.blog
7 |
8 |
9 | def load_comment_list(blog_id: int):
10 | ret = []
11 | for i in read_comment_list(blog_id):
12 | ret.append(Comment(i))
13 | return ret
14 |
15 |
16 | class _Comment:
17 | comment_tuple = namedtuple("Comment", "blog email content update_time")
18 |
19 | @staticmethod
20 | def get_user_comment_count(auth: "object.user"):
21 | return get_user_comment_count(auth.id)
22 |
23 | @staticmethod
24 | def create(blog: "object.blog.BlogArticle", auth: "object.user.User", content):
25 | return create_comment(blog.id, auth.id, content)
26 |
27 |
28 | class Comment(_Comment):
29 | def __init__(self, comment_id):
30 | self.id = comment_id
31 |
32 | @property
33 | def info(self):
34 | return Comment.comment_tuple(*read_comment(self.id))
35 |
36 | @property
37 | def blog(self):
38 | return object.blog.BlogArticle(self.info.blog)
39 |
40 | @property
41 | def auth(self):
42 | return object.user.User(self.info.email)
43 |
44 | @property
45 | def content(self):
46 | return self.info.content
47 |
48 | @property
49 | def update_time(self):
50 | return datetime.utcfromtimestamp(datetime.timestamp(self.info.update_time))
51 |
52 | def is_delete(self):
53 | return not self.auth.is_authenticated and self.blog.is_delete and len(self.content) != 0
54 |
55 | def delete(self):
56 | return delete_comment(self.id)
57 |
--------------------------------------------------------------------------------
/object/msg.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from collections import namedtuple
3 | from datetime import datetime
4 |
5 | from sql.msg import read_msg_list, get_msg_count, create_msg, read_msg, get_user_msg_count, delete_msg
6 | import object.user
7 |
8 |
9 | class _Message:
10 | message_tuple = namedtuple("Message", "email content update_time secret")
11 |
12 | @staticmethod
13 | def get_message_list(limit: Optional[int] = None, offset: Optional[int] = None, show_secret: bool = False):
14 | ret = []
15 | for i in read_msg_list(limit=limit, offset=offset, show_secret=show_secret):
16 | ret.append(Message(i))
17 | return ret
18 |
19 | @staticmethod
20 | def get_msg_count(auth: "object.user.User" = None):
21 | if auth is None:
22 | return get_msg_count()
23 | return get_user_msg_count(auth.id)
24 |
25 | @staticmethod
26 | def create(auth: "object.user.User", content, secret: bool = False):
27 | ret = create_msg(auth.id, content, secret)
28 | if ret is not None:
29 | return Message(ret)
30 | return None
31 |
32 |
33 | class Message(_Message):
34 | def __init__(self, msg_id):
35 | self.id = msg_id
36 |
37 | @property
38 | def info(self):
39 | return Message.message_tuple(*read_msg(self.id))
40 |
41 | @property
42 | def auth(self):
43 | return object.user.User(self.info.email)
44 |
45 | @property
46 | def content(self):
47 | return self.info.content
48 |
49 | @property
50 | def update_time(self):
51 | return datetime.utcfromtimestamp(datetime.timestamp(self.info.update_time))
52 |
53 | @property
54 | def secret(self):
55 | return self.info.secret
56 |
57 | @property
58 | def is_delete(self):
59 | return not self.auth.is_authenticated and len(self.content) != 0
60 |
61 | def delete(self):
62 | return delete_msg(self.id)
63 |
--------------------------------------------------------------------------------
/templates/auth/delete.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %} 删除用户 {% endblock %}
4 |
5 | {% block style %}
6 | {{ super() }}
7 |
8 | {% endblock %}
9 |
10 | {% block content %}
11 |
42 | {% endblock %}
--------------------------------------------------------------------------------
/aliyun/__init__.py:
--------------------------------------------------------------------------------
1 | from configure import conf
2 | import oss2
3 | import logging.handlers
4 | import logging
5 | import os
6 | from urllib.parse import urljoin
7 |
8 |
9 | class Aliyun:
10 | def __init__(self, key, secret, endpoint, name, is_cname):
11 | self.key = key
12 | self.secret = secret
13 | self.auth = oss2.Auth(key, secret)
14 | self.bucket = oss2.Bucket(self.auth, endpoint, name, is_cname=is_cname)
15 | self.logger = logging.getLogger("main.aliyun")
16 | self.logger.setLevel(conf["LOG_LEVEL"])
17 | if len(conf["LOG_HOME"]) > 0:
18 | handle = logging.handlers.TimedRotatingFileHandler(
19 | os.path.join(conf["LOG_HOME"], f"aliyun.log"), backupCount=10)
20 | handle.setFormatter(logging.Formatter(conf["LOG_FORMAT"]))
21 | self.logger.addHandler(handle)
22 |
23 | def upload_file(self, name, f):
24 | res = self.bucket.put_object(name, f)
25 | self.logger.info(f"Upload {name} "
26 | f"id: {res.request_id} status: {res.status} "
27 | f"etag: {res.etag} resp: {res.resp} "
28 | f"version id: {res.versionid} key: {self.key}")
29 |
30 | def shared_obj(self, name, time=15):
31 | if not self.bucket.object_exists(name):
32 | return None
33 | if conf["ALIYUN_BUCKET_USE_SIGN_URL"]:
34 | url = self.bucket.sign_url('GET', name, time, slash_safe=True)
35 | else:
36 | url = urljoin(conf["ALIYUN_BUCKET_ENDPOINT"], name)
37 | self.logger.debug(f"Get url {url} name: {name} time: {time}s key: {self.key}")
38 | return url
39 |
40 |
41 | if conf["USE_ALIYUN"]:
42 | aliyun = Aliyun(conf["ALIYUN_KEY"],
43 | conf["ALIYUN_SECRET"],
44 | conf["ALIYUN_BUCKET_ENDPOINT"],
45 | conf["ALIYUN_BUCKET_NAME"],
46 | conf["ALIYUN_BUCKET_IS_CNAME"])
47 | else:
48 | aliyun = None
49 |
--------------------------------------------------------------------------------
/gunicorn.conf.py:
--------------------------------------------------------------------------------
1 | # gunicorn.conf.py
2 | import os
3 | import multiprocessing
4 | import logging.handlers
5 | import logging
6 |
7 | try:
8 | import gevent.monkey
9 | gevent.monkey.patch_all()
10 | except ImportError:
11 | pass
12 |
13 | bind = '127.0.0.1:5000'
14 | timeout = 30 # 超时
15 |
16 | worker_class = 'gevent'
17 | workers = multiprocessing.cpu_count() * 2 + 1 # 进程数
18 | threads = 2 # 指定每个进程开启的线程数
19 |
20 | hblog_path = os.path.join(os.environ['HOME'], "hblog")
21 | os.makedirs(hblog_path, exist_ok=True, mode=0o775)
22 |
23 | # 设置访问日志和错误信息日志路径
24 | log_format = ("[%(levelname)s]:%(name)s:%(asctime)s "
25 | "(%(filename)s:%(lineno)d %(funcName)s) "
26 | "%(process)d %(thread)d "
27 | "%(message)s")
28 | log_formatter = logging.Formatter(log_format)
29 |
30 | # 错误日志
31 | gunicorn_error_logger = logging.getLogger("gunicorn.error")
32 | gunicorn_error_logger.setLevel(logging.WARNING)
33 |
34 | errorlog = os.path.join(hblog_path, "gunicorn_error.log")
35 | time_handle = logging.handlers.TimedRotatingFileHandler(errorlog, when="d", backupCount=30, encoding='utf-8')
36 | gunicorn_error_logger.addHandler(time_handle)
37 | time_handle.setFormatter(log_formatter)
38 |
39 | # 一般日志
40 | gunicorn_access_logger = logging.getLogger("gunicorn.access")
41 | gunicorn_access_logger.setLevel(logging.INFO)
42 |
43 | accesslog = os.path.join(hblog_path, "gunicorn_access.log")
44 | time_handle = logging.handlers.TimedRotatingFileHandler(accesslog, when="d", backupCount=10, encoding='utf-8')
45 | gunicorn_access_logger.addHandler(time_handle)
46 | time_handle.setFormatter(log_formatter)
47 |
48 | # 输出日志
49 | gunicorn_access_logger.info("Load gunicorn conf success")
50 | gunicorn_access_logger.info(f"bind: {bind}")
51 | gunicorn_access_logger.info(f"timeout: {timeout}")
52 | gunicorn_access_logger.info(f"worker_class: {worker_class}")
53 | gunicorn_access_logger.info(f"workers: {workers}")
54 | gunicorn_access_logger.info(f"threads: {threads}")
55 | gunicorn_access_logger.info(f"hblog_path: {hblog_path}")
56 |
--------------------------------------------------------------------------------
/object/archive.py:
--------------------------------------------------------------------------------
1 | from collections import namedtuple
2 |
3 | import sql.blog # 不用 from import 避免循环导入
4 | from sql.archive import (read_archive,
5 | create_archive,
6 | get_archive_list,
7 | get_blog_archive,
8 | delete_archive,
9 | add_blog_to_archive,
10 | sub_blog_from_archive)
11 | from sql.statistics import get_archive_click
12 |
13 |
14 | class _Archive:
15 | archive_tuple = namedtuple('Archive', 'name describe')
16 |
17 | @staticmethod
18 | def get_archive_list():
19 | ret = []
20 | for i in get_archive_list():
21 | ret.append(Archive(i))
22 | return ret
23 |
24 |
25 | @staticmethod
26 | def get_blog_archive(blog_id: int):
27 | archive_list = []
28 | for i in get_blog_archive(blog_id):
29 | archive_list.append(Archive(i))
30 | return archive_list
31 |
32 | @staticmethod
33 | def create(name, describe):
34 | ret = create_archive(name, describe)
35 | if ret is None:
36 | return None
37 | return Archive(ret)
38 |
39 |
40 | class Archive(_Archive):
41 | def __init__(self, archive_id):
42 | self.id = archive_id
43 |
44 | @property
45 | def info(self):
46 | return Archive.archive_tuple(*read_archive(self.id))
47 |
48 | @property
49 | def name(self):
50 | return self.info.name
51 |
52 | @property
53 | def describe(self):
54 | return self.info.describe
55 |
56 | @property
57 | def count(self):
58 | return sql.blog.get_archive_blog_count(self.id)
59 |
60 | @property
61 | def clicks(self):
62 | return get_archive_click(self.id)
63 |
64 | def is_delete(self):
65 | return len(self.name) != 0
66 |
67 | def delete(self):
68 | return delete_archive(self.id)
69 |
70 | def add_blog(self, blog_id: int):
71 | add_blog_to_archive(blog_id, self.id)
72 |
73 | def sub_blog(self, blog_id: int):
74 | sub_blog_from_archive(blog_id, self.id)
75 |
--------------------------------------------------------------------------------
/sql/base.py:
--------------------------------------------------------------------------------
1 | import abc
2 | from typing import List, Dict, Tuple, Union, Optional
3 | import logging.handlers
4 | import logging
5 | from configure import conf
6 | import os
7 |
8 |
9 | class DBException(Exception):
10 | ...
11 |
12 |
13 | class DBDoneException(DBException):
14 | ...
15 |
16 |
17 | class DBCloseException(DBException):
18 | ...
19 |
20 |
21 | class DBBit:
22 | BIT_0 = b'\x00'
23 | BIT_1 = b'\x01'
24 |
25 |
26 | class Database(metaclass=abc.ABCMeta):
27 | @abc.abstractmethod
28 | def __init__(self, host: str, name: str, passwd: str, port: str):
29 | self._host = str(host)
30 | self._name = str(name)
31 | self._passwd = str(passwd)
32 | if port is None:
33 | self._port = 3306
34 | else:
35 | self._port = int(port)
36 | self.logger = logging.getLogger("main.database")
37 | self.logger.setLevel(conf["LOG_LEVEL"])
38 | if len(conf["LOG_HOME"]) > 0:
39 | handle = logging.handlers.TimedRotatingFileHandler(
40 | os.path.join(conf["LOG_HOME"], f"mysql-{name}@{host}.log"), backupCount=10)
41 | handle.setFormatter(logging.Formatter(conf["LOG_FORMAT"]))
42 | self.logger.addHandler(handle)
43 |
44 | @abc.abstractmethod
45 | def search(self, sql: str, *args, not_commit: bool = False):
46 | """
47 | 执行 查询 SQL语句
48 | :parm sql: SQL语句
49 | :return:
50 | """
51 | ...
52 |
53 | @abc.abstractmethod
54 | def insert(self, sql: str, *args, not_commit: bool = False):
55 | """
56 | 执行 插入 SQL语句, 并提交
57 | :parm sql: SQL语句
58 | :return:
59 | """
60 | ...
61 |
62 | @abc.abstractmethod
63 | def delete(self, sql: str, *args, not_commit: bool = False):
64 | """
65 | 执行 删除 SQL语句, 并提交
66 | :parm sql: SQL语句
67 | :return:
68 | """
69 | ...
70 |
71 | @abc.abstractmethod
72 | def update(self, sql: str, *args, not_commit: bool = False):
73 | """
74 | 执行 更新 SQL语句, 并提交
75 | :parm sql: SQL语句
76 | :return:
77 | """
78 | ...
79 |
--------------------------------------------------------------------------------
/app/index.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, render_template, request
2 | from flask_login import current_user
3 |
4 | from configure import conf
5 | import app
6 | from object.blog import BlogArticle
7 | from object.msg import Message
8 | from sql.statistics import get_hello_click, add_hello_click, get_home_click, add_home_click
9 |
10 | index = Blueprint("base", __name__)
11 |
12 |
13 | @index.route('/')
14 | @app.cache.cached(timeout=conf["VIEW_CACHE_EXPIRE"])
15 | def hello_page():
16 | app.HBlogFlask.print_load_page_log(f"hello")
17 | add_hello_click()
18 | return render_template("index/hello.html")
19 |
20 |
21 | @index.route('/home')
22 | def index_page():
23 | blog_list = BlogArticle.get_blog_list(limit=5, offset=0, not_top=True)
24 | msg_list = Message.get_message_list(limit=6, offset=0, show_secret=False)
25 | app.HBlogFlask.print_load_page_log(f"index")
26 | add_home_click()
27 | return render_template("index/index.html",
28 | blog_list=blog_list,
29 | msg_list=msg_list,
30 | show_email=current_user.check_role("ReadUserInfo"),
31 | hello_clicks=get_hello_click(),
32 | home_clicks=get_home_click())
33 |
34 |
35 | @index.context_processor
36 | @app.cache.cached(timeout=conf["CACHE_EXPIRE"], key_prefix="inject_base:index")
37 | def inject_base():
38 | """ index默认模板变量, 覆盖app变量 """
39 | return {"top_nav": ["active", "", "", "", "", ""]}
40 |
41 |
42 | def get_icp():
43 | for i in conf["ICP"]:
44 | if i in request.host:
45 | return conf["ICP"][i]
46 |
47 |
48 | def get_gongan():
49 | for i in conf["GONG_AN"]:
50 | if i in request.host:
51 | return conf["GONG_AN"][i]
52 |
53 |
54 | @index.app_context_processor
55 | @app.cache.cached(timeout=conf["CACHE_EXPIRE"], key_prefix="inject_base")
56 | def inject_base():
57 | """ app默认模板变量 """
58 | return {"blog_name": conf['BLOG_NAME'],
59 | "top_nav": ["", "", "", "", "", ""],
60 | "blog_describe": conf['BLOG_DESCRIBE'],
61 | "conf": conf,
62 | "get_icp": get_icp,
63 | "get_gongan": get_gongan}
64 |
--------------------------------------------------------------------------------
/sql/cache_refresh.py:
--------------------------------------------------------------------------------
1 | from sql import DB
2 | from configure import conf
3 | from sql.archive import read_archive, get_archive_list_iter, get_blog_archive
4 | from sql.blog import read_blog, get_blog_count, get_archive_blog_count, get_user_blog_count, get_blog_list_iter
5 | from sql.comment import read_comment, read_comment_list_iter, get_user_comment_count
6 | from sql.msg import read_msg, read_msg_list_iter, get_msg_count, get_user_msg_count
7 | from sql.user import (read_user, get_user_list_iter, get_role_list_iter,
8 | get_user_email, get_role_name, check_role, role_authority)
9 | import logging.handlers
10 | import os
11 |
12 | refresh_logger = logging.getLogger("main.refresh")
13 | refresh_logger.setLevel(conf["LOG_LEVEL"])
14 | if len(conf["LOG_HOME"]) > 0:
15 | handle = logging.handlers.TimedRotatingFileHandler(
16 | os.path.join(conf["LOG_HOME"], f"redis-refresh.log"), backupCount=10)
17 | handle.setFormatter(logging.Formatter(conf["LOG_FORMAT"]))
18 | refresh_logger.addHandler(handle)
19 |
20 |
21 | def refresh():
22 | refresh_logger.info("refresh redis cache started.")
23 |
24 | for i in get_archive_list_iter():
25 | read_archive(i[0], not_cache=True)
26 | get_archive_blog_count(i[0], not_cache=True)
27 |
28 | for i in get_blog_list_iter():
29 | read_blog(i[0], not_cache=True)
30 | get_blog_archive(i[0], not_cache=True)
31 | get_blog_count(not_cache=True)
32 |
33 | for i in read_comment_list_iter():
34 | read_comment(i[0], not_cache=True)
35 |
36 | for i in read_msg_list_iter():
37 | read_msg(i[0], not_cache=True)
38 | get_msg_count(not_cache=True)
39 |
40 | for i in get_user_list_iter():
41 | email = get_user_email(i[0], not_cache=True)
42 | get_user_blog_count(i[0], not_cache=True)
43 | get_user_comment_count(i[0], not_cache=True)
44 | get_user_msg_count(i[0], not_cache=True)
45 | read_user(email, not_cache=True)
46 |
47 | for i in get_role_list_iter():
48 | get_role_name(i[0], not_cache=True)
49 | for a in role_authority:
50 | check_role(i[0], a, not_cache=True)
51 |
52 | refresh_logger.info("refresh redis cache finished.")
53 |
--------------------------------------------------------------------------------
/static/styles/index/hello.css:
--------------------------------------------------------------------------------
1 | #base {
2 | position: relative;
3 | }
4 |
5 | @keyframes shaking {
6 | from {
7 | transform: rotate(0deg);
8 | }
9 |
10 | 20% {
11 | transform: rotate(-3deg);
12 | }
13 |
14 | 40% {
15 | transform: rotate(0deg);
16 | }
17 |
18 | 60% {
19 | transform: rotate(3deg);
20 | }
21 |
22 | 80% {
23 | transform: rotate(0deg);
24 | }
25 |
26 | to {
27 | transform: rotate(0deg);
28 | }
29 | }
30 |
31 | #title-1 {
32 | text-decoration: none;
33 | border-bottom: black solid 3px;
34 | padding-bottom: 15px;
35 |
36 | width: 96%;
37 | margin: auto;
38 |
39 | font-weight: bold;
40 | text-align: center;
41 | color: black;
42 | font-size: 50px;
43 | padding-top: 5%;
44 | }
45 |
46 | #title-1:hover {
47 | animation: shaking 300ms linear 0s 1;
48 | }
49 |
50 | #btn {
51 | position: relative;
52 | display: block;
53 | text-align: center;
54 |
55 | border-radius: 10px;
56 | border-width: 3px;
57 | border-style: ridge;
58 | border-color: rgb(123, 104, 238);
59 |
60 | background-image: linear-gradient(to right, #F0FFF0 0%, #FFDC00 30%, #FFDC00 100%);
61 |
62 | font-size: 30px;
63 | height: 100px;
64 |
65 | box-shadow: 5px 5px 2px 0 black;
66 | }
67 |
68 | @media all and (max-width: 992px) {
69 | #btn {
70 | top: 120px;
71 | left: 3%;
72 | width: 94%;
73 | }
74 | }
75 |
76 | @media not all and (max-width: 992px) {
77 | #btn {
78 | top: 120px;
79 | left: 25%;
80 | width: 50%;
81 | transition:
82 | transform 1s,
83 | background-image 1s;
84 | }
85 |
86 | #btn:hover {
87 | background-image: linear-gradient(to right, #F0FFF0 0%, #F0FFF0 70%, #FFDC00 100%);
88 | transform: scale(1.5);
89 | }
90 | }
91 |
92 | #main {
93 | text-align: center;
94 | }
95 |
96 | #title-section {
97 | height: 50vh;
98 | width: 50%;
99 | margin: 10% auto;
100 | background-color: rgb(255, 255, 255, 60%);
101 | border-radius: 10px;
102 | }
--------------------------------------------------------------------------------
/app/oss.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, redirect, render_template, abort, flash, url_for, request
2 | from flask_login import login_required
3 | from flask_wtf import FlaskForm
4 | from wtforms import FileField, StringField, SubmitField
5 | from wtforms.validators import DataRequired, Length
6 | from urllib.parse import urljoin
7 | import oss2
8 |
9 | from aliyun import aliyun
10 | import app
11 |
12 | oss = Blueprint("oss", __name__)
13 |
14 |
15 | class UploadForm(FlaskForm):
16 | file = FileField("选择文件", description="待上传文件",
17 | validators=[DataRequired(message="必须选择文件")])
18 | path = StringField("存储文件夹", description="文件路径(不含文件名)",
19 | validators=[Length(-1, 30, message="文件路径长度为30个字符以内")])
20 | submit = SubmitField("上传")
21 |
22 | def __init__(self):
23 | super(UploadForm, self).__init__()
24 | self.path.data = "hblog/"
25 |
26 |
27 | @oss.before_request
28 | def check_aliyun():
29 | if aliyun is None:
30 | app.HBlogFlask.print_user_opt_fail_log("aliyun not used")
31 | abort(404)
32 | return
33 |
34 |
35 | @oss.route('get/')
36 | def get_page(name: str):
37 | app.HBlogFlask.print_user_opt_success_log(f"get file {name}")
38 | url = aliyun.shared_obj(name)
39 | if url is None:
40 | abort(404)
41 | return redirect(url)
42 |
43 |
44 | @oss.route('upload', methods=["GET", "POST"])
45 | @login_required
46 | @app.role_required("ConfigureSystem", "upload file")
47 | def upload_page():
48 | form = UploadForm()
49 | if form.validate_on_submit():
50 | file = request.files["file"]
51 | path: str = form.path.data
52 | if len(path) > 0 and not path.endswith('/'):
53 | path += "/"
54 | if path.startswith('/'):
55 | path = path[1:]
56 | path += file.filename
57 | try:
58 | aliyun.upload_file(path, file)
59 | except oss2.exceptions.OssError:
60 | app.HBlogFlask.print_sys_opt_success_log(f"Upload file {path} fail")
61 | flash(f"文件 {file.filename} 上传失败")
62 | else:
63 | app.HBlogFlask.print_sys_opt_success_log(f"Upload file {path}")
64 | flash(f"文件 {file.filename} 已上传: {urljoin(request.host_url, url_for('oss.get_page', name=path))}")
65 | return redirect(url_for("oss.upload_page"))
66 | app.HBlogFlask.print_load_page_log(f"OSS upload")
67 | return render_template("oss/upload.html", UploadForm=form)
68 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HBlog
2 | ## 介绍
3 | `HBlog`是基于Python和Flask的博客系统。
4 |
5 | * 具有博文、博文评论、博文归档的功能。
6 | * 具有博客留言的功能。
7 | * 访客注册、角色管理的功能。
8 | * 文件存储的功能。
9 | * 具有管理员管理后台。
10 |
11 | ## 部署
12 | ### 下载
13 | ```shell
14 | $ useradd hblog -m
15 | $ git clone https://github.com/SuperH-0630/HBlog.git /home/hblog/.HBlog
16 | ```
17 |
18 | ### 下载依赖
19 | ```shell
20 | $ sudo -u hblog python3.10 -m pip install -r /home/hblog/.HBlog/requirements.txt --user
21 | $ sudo -u hblog python3.10 -m pip install gunicorn gevent --user
22 | ```
23 |
24 | ### 配置
25 | 创建配置文件`etc/hblog/conf.json`,配置文件内容如下:
26 | ```json
27 | {
28 | "DEBUG_PROFILE": false,
29 | "SECRET_KEY": "随机密钥",
30 |
31 | "BLOG_NAME": "",
32 | "BLOG_DESCRIBE": "",
33 | "INTRODUCE": {
34 | "介绍的名称": "可任意创建更多介绍"
35 | },
36 | "INTRODUCE_LINK": {
37 | "连接名称": "可任意创建更多连接"
38 | },
39 | "ABOUT_ME_PAGE": "about_me.html静态页面的地址",
40 | "FOOT": "页脚信息",
41 |
42 | "MYSQL_URL": "",
43 | "MYSQL_PORT": 3306,
44 | "MYSQL_NAME": "",
45 | "MYSQL_PASSWD": "",
46 | "MYSQL_DATABASE": "",
47 |
48 | "REDIS_HOST": "",
49 | "REDIS_PORT": 6379,
50 | "REDIS_NAME": "",
51 | "REDIS_PASSWD": "",
52 | "REDIS_DATABASE": 0,
53 |
54 | "CACHE_REDIS_HOST": "",
55 | "CACHE_REDIS_PORT": 6379,
56 | "CACHE_REDIS_NAME": "",
57 | "CACHE_REDIS_PASSWD": "",
58 | "CACHE_REDIS_DATABASE": 0,
59 |
60 | "MAIL_SERVER": "SMTP服务地址",
61 | "MAIL_PORT": 465,
62 | "MAIL_USE_TLS": false,
63 | "MAIL_USE_SSL": true,
64 | "MAIL_USERNAME": "",
65 | "MAIL_PASSWORD": "@0630",
66 | "MAIL_SENDER": "名字 <发件人地址>",
67 |
68 | "USE_ALIYUN": true,
69 | "ALIYUN_KEY": "阿里云OOS的账号Key",
70 | "ALIYUN_SECRET": "",
71 | "ALIYUN_BUCKET_ENDPOINT": "",
72 | "ALIYUN_BUCKET_IS_CNAME": false,
73 | "ALIYUN_BUCKET_NAME": "",
74 | "ALIYUN_BUCKET_USE_SIGN_URL": false,
75 |
76 | "LOG_LEVEL": "debug",
77 | "LOG_HOME": "log",
78 | "LOG_SENDER": true,
79 |
80 | "LOGO": "Logo的文件名,存储在static目录的相对路径",
81 | "ICP": {
82 | "备案的域名": "ICP备案"
83 | },
84 | "GONG_AN": {
85 | "备案的域名": "公安备案"
86 | }
87 | }
88 | ```
89 |
90 | ### 静态`AboutMe.html`页面
91 | 一个普通的HTML页面,必须包含`
`,这部分内容会被显示在博客上。
92 |
93 | ### 创建`systemd`服务文件
94 | ```serivce
95 | [Unit]
96 | Description=HBlog server on 8080
97 | After=network.target auditd.service
98 |
99 | [Service]
100 | User=hblog
101 | Group=hblog
102 | WorkingDirectory=/home/hblog/.HBlog/
103 | ExecStart=python3.10的路径 -m gunicorn -c /home/hblog/.HBlog/gunicorn.conf.py main:app --preload -b 0.0.0.0:8080
104 | Type=simple
105 | Environment="HBLOG_CONF=/etc/hblog/conf.json"
106 |
107 | [Install]
108 | WantedBy=multi-user.targe
109 | ```
110 |
111 | ## 样例博客
112 | 我的博客就是用HBlog搭建的,访问:[是桓的小窝](https://www.song-zh.com)。
113 |
--------------------------------------------------------------------------------
/templates/index/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %} 主页 {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
9 |
10 |
11 | {% for info in conf['INTRODUCE'] %}
12 |
{{ info[0] }}
13 | {{ info[1] | safe }}
14 | {% endfor %}
15 |
16 |
17 |
18 |
欢迎页点击量: {{ hello_clicks }}
19 |
首页点击量: {{ home_clicks }}
20 |
21 |
22 |
23 | {% for link in conf['INTRODUCE_LINK'] %}
24 |
{{ link }}
25 | {% endfor %}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | `
34 | {% if current_user.check_role("ReadBlog") %} {# 检查是否具有读取权限 #}
35 | {% if current_user.check_role("ReadMsg") %}
36 |
37 | {% for blog in blog_list %}
38 | {{ render_docx(blog, False) }}
39 | {% endfor %}
40 |
41 | {% else %}
42 |
43 | {% for blog in blog_list %}
44 | {{ render_docx(blog, False) }}
45 | {% endfor %}
46 |
47 | {% endif %}
48 | {% endif %}
49 |
50 | {% if current_user.check_role("ReadMsg") %} {# 检查是否具有读取权限 #}
51 | {% if current_user.check_role("ReadBlog") %}
52 |
57 | {% else %}
58 |
63 | {% endif %}
64 | {% endif %}
65 |
66 |
67 | {% endblock %}
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .idea
2 |
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | *.py[cod]
6 | *$py.class
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | wheels/
25 | share/python-wheels/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 | MANIFEST
30 |
31 | # PyInstaller
32 | # Usually these files are written by a python script from a template
33 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
34 | *.manifest
35 | *.spec
36 |
37 | # Installer logs
38 | pip-log.txt
39 | pip-delete-this-directory.txt
40 |
41 | # Unit test / coverage reports
42 | htmlcov/
43 | .tox/
44 | .nox/
45 | .coverage
46 | .coverage.*
47 | .cache
48 | nosetests.xml
49 | coverage.xml
50 | *.cover
51 | *.py,cover
52 | .hypothesis/
53 | .pytest_cache/
54 | cover/
55 |
56 | # Translations
57 | *.mo
58 | *.pot
59 |
60 | # Django stuff:
61 | *.log
62 | local_settings.py
63 | db.sqlite3
64 | db.sqlite3-journal
65 |
66 | # Flask stuff:
67 | instance/
68 | .webassets-cache
69 |
70 | # Scrapy stuff:
71 | .scrapy
72 |
73 | # Sphinx documentation
74 | docs/_build/
75 |
76 | # PyBuilder
77 | .pybuilder/
78 | target/
79 |
80 | # Jupyter Notebook
81 | .ipynb_checkpoints
82 |
83 | # IPython
84 | proarchive_default/
85 | ipython_config.py
86 |
87 | # pyenv
88 | # For a library or package, you might want to ignore these files since the code is
89 | # intended to run in multiple environments; otherwise, check them in:
90 | # .python-version
91 |
92 | # pipenv
93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
96 | # install all needed dependencies.
97 | #Pipfile.lock
98 |
99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
100 | __pypackages__/
101 |
102 | # Celery stuff
103 | celerybeat-schedule
104 | celerybeat.pid
105 |
106 | # SageMath parsed files
107 | *.sage.py
108 |
109 | # Environments
110 | .env
111 | .venv
112 | env/
113 | venv/
114 | ENV/
115 | env.bak/
116 | venv.bak/
117 |
118 | # Spyder project settings
119 | .spyderproject
120 | .spyproject
121 |
122 | # Rope project settings
123 | .ropeproject
124 |
125 | # mkdocs documentation
126 | /site
127 |
128 | # mypy
129 | .mypy_cache/
130 | .dmypy.json
131 | dmypy.json
132 |
133 | # Pyre type checker
134 | .pyre/
135 |
136 | # pytype static type analyzer
137 | .pytype/
138 |
139 | # Cython debug symbols
140 | cython_debug/
141 |
142 | conf.json
--------------------------------------------------------------------------------
/templates/auth/yours.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %} 关于你 {% endblock %}
4 |
5 | {% block style %}
6 | {{ super() }}
7 |
8 | {% endblock %}
9 |
10 | {% block content %}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
{{ current_user.email }}
18 |
用户组:{{ current_user.role_name }}
19 |
评论条数:{{ comment_count }}
20 |
留言条数:{{ msg_count }}
21 |
博客:{{ blog_count }}
22 |
23 |
39 |
40 |
修改密码
41 |
退出登录
42 | {% if current_user.check_role('DeleteUser') %}
43 |
删除用户
44 | {% endif %}
45 | {% if current_user.check_role('ConfigureSystem') %}
46 |
角色设置
47 |
上传文件
48 | {% endif %}
49 |
50 |
51 |
52 |
53 |
54 | {% endblock %}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 |
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | *.py[cod]
6 | *$py.class
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | wheels/
25 | share/python-wheels/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 | MANIFEST
30 |
31 | # PyInstaller
32 | # Usually these files are written by a python script from a template
33 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
34 | *.manifest
35 | *.spec
36 |
37 | # Installer logs
38 | pip-log.txt
39 | pip-delete-this-directory.txt
40 |
41 | # Unit test / coverage reports
42 | htmlcov/
43 | .tox/
44 | .nox/
45 | .coverage
46 | .coverage.*
47 | .cache
48 | nosetests.xml
49 | coverage.xml
50 | *.cover
51 | *.py,cover
52 | .hypothesis/
53 | .pytest_cache/
54 | cover/
55 |
56 | # Translations
57 | *.mo
58 | *.pot
59 |
60 | # Django stuff:
61 | *.log
62 | log
63 | local_settings.py
64 | db.sqlite3
65 | db.sqlite3-journal
66 |
67 | # Flask stuff:
68 | instance/
69 | .webassets-cache
70 |
71 | # Scrapy stuff:
72 | .scrapy
73 |
74 | # Sphinx documentation
75 | docs/_build/
76 |
77 | # PyBuilder
78 | .pybuilder/
79 | target/
80 |
81 | # Jupyter Notebook
82 | .ipynb_checkpoints
83 |
84 | # IPython
85 | proarchive_default/
86 | ipython_config.py
87 |
88 | # pyenv
89 | # For a library or package, you might want to ignore these files since the code is
90 | # intended to run in multiple environments; otherwise, check them in:
91 | # .python-version
92 |
93 | # pipenv
94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
97 | # install all needed dependencies.
98 | #Pipfile.lock
99 |
100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
101 | __pypackages__/
102 |
103 | # Celery stuff
104 | celerybeat-schedule
105 | celerybeat.pid
106 |
107 | # SageMath parsed files
108 | *.sage.py
109 |
110 | # Environments
111 | .env
112 | .venv
113 | env/
114 | venv/
115 | ENV/
116 | env.bak/
117 | venv.bak/
118 |
119 | # Spyder project settings
120 | .spyderproject
121 | .spyproject
122 |
123 | # Rope project settings
124 | .ropeproject
125 |
126 | # mkdocs documentation
127 | /site
128 |
129 | # mypy
130 | .mypy_cache/
131 | .dmypy.json
132 | dmypy.json
133 |
134 | # Pyre type checker
135 | .pyre/
136 |
137 | # pytype static type analyzer
138 | .pytype/
139 |
140 | # Cython debug symbols
141 | cython_debug/
142 |
143 | etc
144 | static/logo.jpg
145 | static/wangan.png
--------------------------------------------------------------------------------
/templates/index/hello.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %} 欢迎 {% endblock %}
4 |
5 | {% block style %}
6 | {{ super() }}
7 |
8 |
15 | {% endblock %}
16 |
17 | {% block nav %} {% endblock %}
18 |
19 | {% block content %}
20 |
21 | 欢迎,这里是《{{ blog_name }}》
22 |
25 |
26 | {% endblock %}
27 |
28 | {% block footer %}
29 | {% if get_icp() %}
30 |
43 |
44 |
68 | {% endif %}
69 | {% endblock %}
--------------------------------------------------------------------------------
/configure/__init__.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import os
4 | conf = {
5 | "DEBUG_PROFILE": False,
6 | "SECRET_KEY": "HBlog-R-Salt",
7 | "BLOG_NAME": "HBlog",
8 | "BLOG_DESCRIBE": "Huan Blog.",
9 | "FOOT": "Power by HBlog",
10 | "ABOUT_ME_PAGE": "",
11 | "INTRODUCTION": "",
12 | "INTRODUCTION_LINK": "",
13 | "MYSQL_URL": "localhost",
14 | "MYSQL_NAME": "localhost",
15 | "MYSQL_PASSWD": "123456",
16 | "MYSQL_PORT": 3306,
17 | "MYSQL_DATABASE": "HBlog",
18 | "REDIS_HOST": "localhost",
19 | "REDIS_PORT": 6379,
20 | "REDIS_NAME": "localhost",
21 | "REDIS_PASSWD": "123456",
22 | "REDIS_DATABASE": 0,
23 | "CACHE_REDIS_HOST": "localhost",
24 | "CACHE_REDIS_PORT": 6379,
25 | "CACHE_REDIS_NAME": "localhost",
26 | "CACHE_REDIS_PASSWD": "123456",
27 | "CACHE_REDIS_DATABASE": 0,
28 | "CACHE_EXPIRE": 604800, # 默认七天过期
29 | "CACHE_REFRESH_INTERVAL": 432000, # 缓存刷新时间 默认五天刷新一次
30 | "VIEW_CACHE_EXPIRE": 60, # 视图函数
31 | "LIST_CACHE_EXPIRE": 5, # 列表 排行
32 | "REDIS_PREFIX": "statistics",
33 | "CACHE_PREFIX": "hblog_cache",
34 | "FLASK_CACHE_PREFIX": "flask_cache",
35 | "MAIL_SERVER": "",
36 | "MAIL_PORT": "",
37 | "MAIL_USE_TLS": False,
38 | "MAIL_USE_SSL": False,
39 | "MAIL_PASSWORD": "",
40 | "MAIL_USERNAME": "",
41 | "MAIL_PREFIX": "",
42 | "MAIL_SENDER": "",
43 | "USE_ALIYUN": False,
44 | "ALIYUN_KEY": "",
45 | "ALIYUN_SECRET": "",
46 | "ALIYUN_BUCKET_ENDPOINT": "",
47 | "ALIYUN_BUCKET_NAME": "",
48 | "ALIYUN_BUCKET_IS_CNAME": False,
49 | "ALIYUN_BUCKET_USE_SIGN_URL": True,
50 | "LOG_HOME": "",
51 | "LOG_FORMAT": "[%(levelname)s]:%(name)s:%(asctime)s "
52 | "(%(filename)s:%(lineno)d %(funcName)s) "
53 | "%(process)d %(thread)d "
54 | "%(message)s",
55 | "LOG_LEVEL": logging.INFO,
56 | "LOG_STDERR": True,
57 | "LOGO": "logo.jpg",
58 | "ICP": {},
59 | "GONG_AN": {},
60 | }
61 |
62 |
63 |
64 | def configure(conf_file: str, encoding="utf-8"):
65 | """ 运行配置程序, 该函数需要在其他模块被执行前调用 """
66 | with open(conf_file, mode="r", encoding=encoding) as f:
67 | json_str = f.read()
68 | conf.update(json.loads(json_str))
69 | if type(conf["LOG_LEVEL"]) is str:
70 | conf["LOG_LEVEL"] = {"debug": logging.DEBUG,
71 | "info": logging.INFO,
72 | "warning": logging.WARNING,
73 | "error": logging.ERROR}.get(conf["LOG_LEVEL"])
74 |
75 | introduce = conf["INTRODUCE"]
76 | introduce_list = []
77 | for i in introduce:
78 | describe: str = introduce[i]
79 | describe = " ".join([f"{i}
" for i in describe.split('\n')])
80 | introduce_list.append((i, describe))
81 | conf["INTRODUCE"] = introduce_list
82 | if len(conf["LOG_HOME"]) > 0:
83 | os.makedirs(conf["LOG_HOME"], exist_ok=True)
84 |
--------------------------------------------------------------------------------
/sql/comment.py:
--------------------------------------------------------------------------------
1 | from sql import db, DB
2 | from sql.cache import (get_comment_from_cache, write_comment_to_cache, delete_comment_from_cache,
3 | get_user_comment_count_from_cache, write_user_comment_count_to_cache,
4 | delete_all_user_comment_count_from_cache, delete_user_comment_count_from_cache)
5 |
6 |
7 | def read_comment_list(blog_id: int, mysql: DB = db):
8 | """ 读取文章的 comment """
9 | cur = mysql.search("SELECT CommentID "
10 | "FROM comment_user "
11 | "WHERE BlogID=%s "
12 | "ORDER BY UpdateTime DESC", blog_id)
13 | if cur is None or cur.rowcount == 0:
14 | return []
15 | return [i[0] for i in cur.fetchall()]
16 |
17 |
18 | def read_comment_list_iter(mysql: DB = db):
19 | """ 读取文章的 comment """
20 | cur = mysql.search("SELECT CommentID "
21 | "FROM comment_user "
22 | "ORDER BY UpdateTime DESC")
23 | if cur is None or cur.rowcount == 0:
24 | return []
25 | return cur
26 |
27 |
28 | def create_comment(blog_id: int, user_id: int, content: str, mysql: DB = db):
29 | """ 新建 comment """
30 | delete_user_comment_count_from_cache(user_id)
31 |
32 | cur = mysql.insert("INSERT INTO comment(BlogID, Auth, Content) "
33 | "VALUES (%s, %s, %s)", blog_id, user_id, content)
34 | if cur is None or cur.rowcount == 0:
35 | return False
36 | read_comment(cur.lastrowid, mysql) # 刷新缓存
37 | return True
38 |
39 |
40 | def read_comment(comment_id: int, mysql: DB = db, not_cache=False):
41 | """ 读取 comment """
42 | if not not_cache:
43 | res = get_comment_from_cache(comment_id)
44 | if res is not None:
45 | return res
46 |
47 | cur = mysql.search("SELECT BlogID, Email, Content, UpdateTime FROM comment_user WHERE CommentID=%s", comment_id)
48 | if cur is None or cur.rowcount == 0:
49 | return [-1, "", "", 0]
50 |
51 | res = cur.fetchone()
52 | write_comment_to_cache(comment_id, *res)
53 | return res
54 |
55 |
56 | def delete_comment(comment_id: int, mysql: DB = db):
57 | """ 删除评论 """
58 | delete_comment_from_cache(comment_id)
59 | delete_all_user_comment_count_from_cache()
60 | cur = mysql.delete("DELETE FROM comment WHERE ID=%s", comment_id)
61 | if cur is None or cur.rowcount == 0:
62 | return False
63 | return True
64 |
65 |
66 | def get_user_comment_count(user_id: int, mysql: DB = db, not_cache=False):
67 | """ 读取指定用户的 comment 个数 """
68 | if not not_cache:
69 | res = get_user_comment_count_from_cache(user_id)
70 | if res is not None:
71 | return res
72 |
73 | cur = mysql.search("SELECT COUNT(*) FROM comment WHERE Auth=%s", user_id)
74 | if cur is None or cur.rowcount == 0:
75 | return 0
76 |
77 | res = cur.fetchone()[0]
78 | write_user_comment_count_to_cache(user_id, res)
79 | return res
80 |
--------------------------------------------------------------------------------
/templates/msg/msg.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %} 留言 {% endblock %}
4 |
5 | {% block style %}
6 | {{ super() }}
7 |
8 | {% endblock %}
9 |
10 | {% block content %}
11 |
12 |
49 |
50 | {% if current_user.check_role("ReadMsg") %} {# 检查是否具有读取权限 #}
51 |
52 |
53 | {% for msg in msg_list %}
54 | {{ render_msg(msg, show_delete) }}
55 | {% endfor %}
56 |
57 |
58 |
59 |
62 | {% endif %}
63 |
64 | {% endblock %}
--------------------------------------------------------------------------------
/app/archive.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, render_template, redirect, url_for, flash, g, request
2 | from flask_login import login_required, current_user
3 | from flask_wtf import FlaskForm
4 | from wtforms import StringField, TextAreaField, SubmitField
5 | from wtforms.validators import DataRequired, Length, ValidationError
6 |
7 | import app
8 | from object.archive import Archive
9 | from configure import conf
10 |
11 | archive = Blueprint("archive", __name__)
12 |
13 |
14 | class CreateArchiveForm(FlaskForm):
15 | name = StringField("名称", description="归档名称",
16 | validators=[DataRequired(message="必须填写归档名称"),
17 | Length(1, 10, message="归档名称长度1-10个字符")])
18 | describe = TextAreaField("描述", description="归档描述",
19 | validators=[Length(-1, 25, message="归档描述长度25个字符以内")])
20 | submit = SubmitField("创建归档")
21 |
22 | def validate_name(self, field):
23 | name = field.data
24 | archive_list = Archive.get_archive_list()
25 | for i in archive_list:
26 | if name == i.name:
27 | raise ValidationError("归档已经存在")
28 |
29 |
30 | def __load_archive_page(form: CreateArchiveForm):
31 | archive_list = Archive.get_archive_list()
32 | app.HBlogFlask.print_load_page_log("archive list")
33 | return render_template("archive/archive.html",
34 | archive_list=archive_list,
35 | form=form,
36 | show_delete=current_user.check_role("DeleteBlog"))
37 |
38 |
39 | @archive.route('/')
40 | def archive_page():
41 | return __load_archive_page(CreateArchiveForm())
42 |
43 |
44 | @archive.route("/create", methods=["POST"])
45 | @login_required
46 | @app.form_required(CreateArchiveForm, "create archive", __load_archive_page)
47 | @app.role_required("WriteBlog", "create archive")
48 | def create_archive_page():
49 | form: CreateArchiveForm = g.form
50 | name = form.name.data
51 | describe = form.describe.data
52 | if Archive.create(name, describe):
53 | app.HBlogFlask.print_sys_opt_success_log(f"Create archive {name}")
54 | flash(f"创建归档 {name} 成功")
55 | else:
56 | app.HBlogFlask.print_sys_opt_fail_log(f"Create archive {name}")
57 | flash(f"创建归档 {name} 失败")
58 | return redirect(url_for("archive.archive_page"))
59 |
60 |
61 | @archive.route("/delete")
62 | @login_required
63 | @app.role_required("DeleteBlog", "delete archive")
64 | def delete_archive_page():
65 | archive_id = int(request.args.get("archive", 1, type=int))
66 | if Archive(archive_id).delete():
67 | app.HBlogFlask.print_sys_opt_success_log(f"Delete archive {archive_id}")
68 | flash("归档删除成功")
69 | else:
70 | app.HBlogFlask.print_sys_opt_fail_log(f"Delete archive {archive_id}")
71 | flash("归档删除失败")
72 | return redirect(url_for("archive.archive_page"))
73 |
74 |
75 | @archive.context_processor
76 | @app.cache.cached(timeout=conf["CACHE_EXPIRE"], key_prefix="inject_base:archive")
77 | def inject():
78 | """ archive 默认模板变量 """
79 | return {"top_nav": ["", "active", "", "", "", ""]}
80 |
--------------------------------------------------------------------------------
/app/msg.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, render_template, abort, redirect, url_for, flash, g, request
2 | from flask_wtf import FlaskForm
3 | from flask_login import login_required, current_user
4 | from wtforms import TextAreaField, BooleanField, SubmitField
5 | from wtforms.validators import DataRequired, Length
6 |
7 | import app
8 | from sql.base import DBBit
9 | from object.msg import Message
10 | from configure import conf
11 |
12 | msg = Blueprint("msg", __name__)
13 |
14 |
15 | class WriteForm(FlaskForm):
16 | """
17 | 写新内容表单
18 | """
19 | content = TextAreaField("", description="留言正文",
20 | validators=[
21 | DataRequired("请输入留言的内容"),
22 | Length(1, 100, message="留言长度1-100个字符")])
23 | secret = BooleanField("私密留言")
24 | submit = SubmitField("留言")
25 |
26 |
27 | def __load_msg_page(page: int, form: WriteForm):
28 | if page < 1:
29 | app.HBlogFlask.print_user_opt_fail_log(f"Load msg list with error page({page})")
30 | abort(404)
31 | return
32 |
33 | msg_list = Message.get_message_list(20, (page - 1) * 20,
34 | show_secret=current_user.check_role("ReadSecretMsg")) # 判断是否可读取私密内容
35 | max_page = app.HBlogFlask.get_max_page(Message.get_msg_count(), 20)
36 | page_list = app.HBlogFlask.get_page("msg.msg_page", page, max_page)
37 | app.HBlogFlask.print_load_page_log(f"msg (page: {page})")
38 | return render_template("msg/msg.html",
39 | msg_list=msg_list,
40 | page=page,
41 | cache_str=f":{page}",
42 | page_list=page_list,
43 | form=form,
44 | show_delete=current_user.check_role("DeleteMsg"),
45 | show_email=current_user.check_role("ReadUserInfo"))
46 |
47 |
48 | @msg.route('/')
49 | def msg_page():
50 | page = request.args.get("page", 1, type=int)
51 | return __load_msg_page(page, WriteForm())
52 |
53 |
54 | @msg.route('/create', methods=["POST"])
55 | @login_required
56 | @app.form_required(WriteForm,
57 | "write msg",
58 | lambda form: __load_msg_page(request.args.get("page", 1, type=int), form))
59 | @app.role_required("WriteMsg", "write msg")
60 | def write_msg_page():
61 | form: WriteForm = g.form
62 | content = form.content.data
63 | secret = form.secret.data
64 | if Message.create(current_user, content, secret):
65 | app.HBlogFlask.print_user_opt_success_log("write msg")
66 | flash("留言成功")
67 | else:
68 | app.HBlogFlask.print_user_opt_fail_log("write msg")
69 | flash("留言失败")
70 | return redirect(url_for("msg.msg_page", page=1))
71 |
72 |
73 | @msg.route('/delete')
74 | @login_required
75 | @app.role_required("DeleteMsg", "delete msg")
76 | def delete_msg_page():
77 | msg_id = request.args.get("msg", 1, type=int)
78 | if Message(msg_id).delete():
79 | app.HBlogFlask.print_user_opt_success_log("delete msg")
80 | flash("留言删除成功")
81 | else:
82 | app.HBlogFlask.print_user_opt_fail_log("delete msg")
83 | flash("留言删除失败")
84 | return redirect(url_for("msg.msg_page", page=1))
85 |
86 |
87 | @msg.context_processor
88 | @app.cache.cached(timeout=conf["CACHE_EXPIRE"], key_prefix="inject_base:msg")
89 | def inject_base():
90 | """ msg 默认模板变量 """
91 | return {"top_nav": ["", "", "", "active", "", ""]}
92 |
--------------------------------------------------------------------------------
/object/blog.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | from collections import namedtuple
3 | from datetime import datetime
4 |
5 | from sql.blog import (get_blog_list,
6 | get_blog_count,
7 | get_archive_blog_list,
8 | get_archive_blog_count,
9 | get_blog_list_not_top,
10 | read_blog,
11 | update_blog,
12 | create_blog,
13 | delete_blog,
14 | set_blog_top,
15 | get_user_blog_count)
16 | from sql.statistics import get_blog_click
17 | from sql.archive import add_blog_to_archive, sub_blog_from_archive
18 | from sql.user import get_user_email
19 | from sql.base import DBBit
20 | import object.user
21 | import object.archive
22 | import object.comment
23 |
24 |
25 | class LoadBlogError(Exception):
26 | pass
27 |
28 |
29 | class _BlogArticle:
30 | article_tuple = namedtuple("Article", "auth title subtitle content update_time create_time top")
31 |
32 | @staticmethod
33 | def get_blog_list(archive_id=None, limit=None, offset=None, not_top=False):
34 | if archive_id is None:
35 | if not_top:
36 | res = get_blog_list_not_top(limit=limit, offset=offset)
37 | else:
38 | res = get_blog_list(limit=limit, offset=offset)
39 | else:
40 | res = get_archive_blog_list(archive_id, limit=limit, offset=offset)
41 |
42 | ret = []
43 | for i in res:
44 | ret.append(BlogArticle(i))
45 | return ret
46 |
47 | @staticmethod
48 | def get_blog_count(archive_id=None, auth=None):
49 | if archive_id is None and auth is None:
50 | return get_blog_count()
51 | if auth is None:
52 | return get_archive_blog_count(archive_id)
53 | return get_user_blog_count(auth.id)
54 |
55 | @staticmethod
56 | def create(title, subtitle, content, archive: "List[object.archive.Archive]", user: "object.user.User"):
57 | return create_blog(user.id, title, subtitle, content, archive)
58 |
59 |
60 | class BlogArticle(_BlogArticle):
61 | def __init__(self, blog_id):
62 | self.id = blog_id
63 |
64 | @property
65 | def info(self):
66 | return BlogArticle.article_tuple(*read_blog(self.id))
67 |
68 | @property
69 | def user(self):
70 | return object.user.User(self.info.auth, is_id=True)
71 |
72 | @property
73 | def title(self):
74 | return self.info.title
75 |
76 | @property
77 | def subtitle(self):
78 | return self.info.subtitle
79 |
80 | @property
81 | def content(self):
82 | return self.info.content
83 |
84 | @property
85 | def update_time(self):
86 | return datetime.utcfromtimestamp(datetime.timestamp(self.info.update_time))
87 |
88 | @property
89 | def create_time(self):
90 | return datetime.utcfromtimestamp(datetime.timestamp(self.info.create_time))
91 |
92 | @property
93 | def clicks(self):
94 | return get_blog_click(self.id)
95 |
96 | @property
97 | def top(self):
98 | return self.info.top
99 |
100 | @top.setter
101 | def top(self, top: bool):
102 | set_blog_top(self.id, top)
103 |
104 | @property
105 | def comment(self):
106 | return object.comment.load_comment_list(self.id)
107 |
108 | @property
109 | def archive(self):
110 | return object.archive.Archive.get_blog_archive(self.id)
111 |
112 | @property
113 | def is_delete(self):
114 | return not self.user.is_authenticated and len(self.content) != 0
115 |
116 | def delete(self):
117 | return delete_blog(self.id)
118 |
119 | def update(self, content: str):
120 | if update_blog(self.id, content):
121 | return True
122 | return False
123 |
124 | def add_to_archive(self, archive_id: int):
125 | return add_blog_to_archive(self.id, archive_id)
126 |
127 | def sub_from_archive(self, archive_id: int):
128 | return sub_blog_from_archive(self.id, archive_id)
129 |
--------------------------------------------------------------------------------
/sql/archive.py:
--------------------------------------------------------------------------------
1 | from sql import db, DB
2 | from sql.cache import (get_archive_from_cache, write_archive_to_cache, delete_archive_from_cache,
3 | get_blog_archive_from_cache, write_blog_archive_to_cache, delete_blog_archive_from_cache,
4 | delete_all_blog_archive_from_cache)
5 | from typing import Optional
6 |
7 |
8 | def create_archive(name: str, describe: str, mysql: DB = db):
9 | """ 创建新归档 """
10 | cur = mysql.insert("INSERT INTO archive(Name, DescribeText) "
11 | "VALUES (%s, %s)", name, describe)
12 | if cur is None or cur.rowcount == 0:
13 | return None
14 | read_archive(cur.lastrowid, mysql)
15 | return cur.lastrowid
16 |
17 |
18 | def read_archive(archive_id: int, mysql: DB = db, not_cache=False):
19 | """ 获取归档 ID """
20 | if not not_cache:
21 | res = get_archive_from_cache(archive_id)
22 | if res is not None:
23 | return res
24 |
25 | cur = mysql.search("SELECT Name, DescribeText "
26 | "FROM archive "
27 | "WHERE ID=%s", archive_id)
28 | if cur is None or cur.rowcount == 0:
29 | return ["", ""]
30 |
31 | res = cur.fetchone()
32 | write_archive_to_cache(archive_id, *res)
33 | return res
34 |
35 |
36 | def get_blog_archive(blog_id: int, mysql: DB = db, not_cache=False):
37 | """ 获取文章的归档 """
38 | if not not_cache:
39 | res = get_blog_archive_from_cache(blog_id)
40 | if res is not None:
41 | return res
42 |
43 | cur = mysql.search("SELECT ArchiveID FROM blog_archive_with_name "
44 | "WHERE BlogID=%s "
45 | "ORDER BY ArchiveName", blog_id)
46 | if cur is None:
47 | return []
48 |
49 | res = [i[0] for i in cur.fetchall()]
50 | write_blog_archive_to_cache(blog_id, res)
51 | return res
52 |
53 |
54 | def delete_archive(archive_id: int, mysql: DB = db):
55 | delete_archive_from_cache(archive_id)
56 | delete_all_blog_archive_from_cache()
57 | conn = mysql.get_connection()
58 |
59 | cur = mysql.delete("DELETE FROM blog_archive WHERE ArchiveID=%s", archive_id, connection=conn)
60 | if cur is None:
61 | conn.rollback()
62 | conn.close()
63 | return False
64 |
65 | cur = mysql.delete("DELETE FROM archive WHERE ID=%s", archive_id, connection=conn)
66 | if cur is None or cur.rowcount == 0:
67 | conn.rollback()
68 | conn.close()
69 | return False
70 |
71 | conn.commit()
72 | conn.close()
73 | return True
74 |
75 |
76 | def add_blog_to_archive(blog_id: int, archive_id: int, mysql: DB = db):
77 | delete_blog_archive_from_cache(blog_id)
78 | cur = mysql.search("SELECT BlogID FROM blog_archive WHERE BlogID=%s AND ArchiveID=%s", blog_id, archive_id)
79 | if cur is None:
80 | return False
81 | if cur.rowcount > 0:
82 | return True
83 | cur = mysql.insert("INSERT INTO blog_archive(BlogID, ArchiveID) VALUES (%s, %s)", blog_id, archive_id)
84 | if cur is None or cur.rowcount != 1:
85 | return False
86 | return True
87 |
88 |
89 | def sub_blog_from_archive(blog_id: int, archive_id: int, mysql: DB = db):
90 | delete_blog_archive_from_cache(blog_id)
91 | cur = mysql.delete("DELETE FROM blog_archive WHERE BlogID=%s AND ArchiveID=%s", blog_id, archive_id)
92 | if cur is None:
93 | return False
94 | return True
95 |
96 |
97 | def get_archive_list(limit: Optional[int] = None, offset: Optional[int] = None, mysql: DB = db):
98 | """ 获取归档列表 """
99 | if limit is not None and offset is not None:
100 | cur = mysql.search("SELECT ID "
101 | "FROM archive "
102 | "ORDER BY Name "
103 | "LIMIT %s "
104 | "OFFSET %s ", limit, offset)
105 | else:
106 | cur = mysql.search("SELECT ID "
107 | "FROM archive "
108 | "ORDER BY Name")
109 |
110 | if cur is None or cur.rowcount == 0:
111 | return []
112 | return [i[0] for i in cur.fetchall()]
113 |
114 |
115 |
116 | def get_archive_list_iter(mysql: DB = db):
117 | """ 获取归档列表 """
118 | cur = mysql.search("SELECT ID "
119 | "FROM archive "
120 | "ORDER BY Name")
121 |
122 | if cur is None or cur.rowcount == 0:
123 | return []
124 | return cur
125 |
--------------------------------------------------------------------------------
/templates/archive/archive.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %} 归档 {% endblock %}
4 |
5 | {% block style %}
6 | {{ super() }}
7 |
8 | {% endblock %}
9 |
10 | {% block content %}
11 | {% if form and current_user.check_role("WriteBlog") %}
12 |
48 | {% endif %}
49 |
50 | {% if current_user.check_role("ReadBlog") %}
51 |
52 | {% for archive in archive_list %}
53 |
54 |
{{ archive.name }}
55 |
56 |
{{ archive.describe }}
57 |
篇数: {{ archive.count }}
58 |
点击量: {{ archive.clicks }}
59 |
进入
60 | {% if show_delete %}
61 |
62 |
63 |
64 |
67 |
68 |
是否确认删除归档 {{ archive.name }}?
69 |
70 |
75 |
76 |
77 |
78 |
79 |
81 | {% endif %}
82 |
83 | {% endfor %}
84 |
85 | {% endif %}
86 | {% endblock %}
--------------------------------------------------------------------------------
/sql/msg.py:
--------------------------------------------------------------------------------
1 | from sql import db, DB
2 | from sql.base import DBBit
3 | from sql.cache import (get_msg_from_cache, write_msg_to_cache, delete_msg_from_cache,
4 | get_msg_cout_from_cache, write_msg_count_to_cache, delete_msg_count_from_cache,
5 | get_user_msg_count_from_cache, write_user_msg_count_to_cache,
6 | delete_all_user_msg_count_from_cache, delete_user_msg_count_from_cache)
7 |
8 | from typing import Optional
9 |
10 |
11 | def read_msg_list(limit: Optional[int] = None,
12 | offset: Optional[int] = None,
13 | show_secret: bool = False,
14 | mysql: DB = db):
15 | if show_secret:
16 | if limit is not None and offset is not None:
17 | cur = mysql.search("SELECT MsgID "
18 | "FROM message_user "
19 | "ORDER BY UpdateTime DESC "
20 | "LIMIT %s "
21 | "OFFSET %s", limit, offset)
22 | else:
23 | cur = mysql.search("SELECT MsgID "
24 | "FROM message_user "
25 | "ORDER BY UpdateTime DESC")
26 | else:
27 | if limit is not None and offset is not None:
28 | cur = mysql.search("SELECT MsgID "
29 | "FROM message_user "
30 | "WHERE Secret=0 "
31 | "ORDER BY UpdateTime DESC "
32 | "LIMIT %s "
33 | "OFFSET %s", limit, offset)
34 | else:
35 | cur = mysql.search("SELECT MsgID "
36 | "FROM message_user "
37 | "WHERE Secret=0 "
38 | "ORDER BY UpdateTime DESC")
39 | if cur is None or cur.rowcount == 0:
40 | return []
41 | return [i[0] for i in cur.fetchall()]
42 |
43 |
44 | def read_msg_list_iter(mysql: DB = db):
45 | cur = mysql.search("SELECT MsgID "
46 | "FROM message_user "
47 | "ORDER BY UpdateTime DESC")
48 | if cur is None or cur.rowcount == 0:
49 | return []
50 | return cur
51 |
52 |
53 | def create_msg(auth: int, content: str, secret: bool = False, mysql: DB = db):
54 | delete_msg_count_from_cache()
55 | delete_user_msg_count_from_cache(auth)
56 |
57 | cur = mysql.insert("INSERT INTO message(Auth, Content, Secret) "
58 | "VALUES (%s, %s, %s)", auth, content, 1 if secret else 0)
59 | if cur is None or cur.rowcount != 1:
60 | return None
61 | read_msg(cur.lastrowid, mysql) # 刷新缓存
62 | return cur.lastrowid
63 |
64 |
65 | def read_msg(msg_id: int, mysql: DB = db, not_cache=False):
66 | if not not_cache:
67 | res = get_msg_from_cache(msg_id)
68 | if res is not None:
69 | return res
70 |
71 | cur = mysql.search("SELECT Email, Content, UpdateTime, Secret "
72 | "FROM message_user "
73 | "WHERE MsgID=%s", msg_id)
74 | if cur is None or cur.rowcount == 0:
75 | return ["", "", "0", False]
76 |
77 | res = cur.fetchone()
78 | write_msg_to_cache(msg_id, *res, is_db_bit=True)
79 | return [*res[:3], res[-1] == DBBit.BIT_1]
80 |
81 |
82 | def delete_msg(msg_id: int, mysql: DB = db):
83 | delete_msg_from_cache(msg_id)
84 | delete_msg_count_from_cache()
85 | delete_all_user_msg_count_from_cache()
86 | cur = mysql.delete("DELETE FROM message WHERE ID=%s", msg_id)
87 | if cur is None or cur.rowcount == 0:
88 | return False
89 | return True
90 |
91 |
92 | def get_msg_count(mysql: DB = db, not_cache=False):
93 | if not not_cache:
94 | res = get_msg_cout_from_cache()
95 | if res is not None:
96 | return res
97 |
98 | cur = mysql.search("SELECT COUNT(*) FROM message")
99 | if cur is None or cur.rowcount == 0:
100 | return 0
101 | res = cur.fetchone()[0]
102 | write_msg_count_to_cache(res)
103 | return res
104 |
105 |
106 | def get_user_msg_count(user_id: int, mysql: DB = db, not_cache=False):
107 | if not not_cache:
108 | res = get_user_msg_count_from_cache(user_id)
109 | if res is not None:
110 | return res
111 |
112 | cur = mysql.search("SELECT COUNT(*) FROM message WHERE Auth=%s", user_id)
113 | if cur is None or cur.rowcount == 0:
114 | return 0
115 | res = cur.fetchone()[0]
116 | write_user_msg_count_to_cache(user_id, res)
117 | return res
118 |
--------------------------------------------------------------------------------
/sql/mysql.py:
--------------------------------------------------------------------------------
1 | import pymysql
2 | from dbutils.pooled_db import PooledDB
3 | from dbutils.steady_db import SteadyDBCursor
4 | from sql.base import Database, DBException, DBCloseException
5 | from typing import Optional, Union
6 | import inspect
7 |
8 |
9 | class MysqlConnectException(DBCloseException):
10 | """Mysql Connect error"""
11 |
12 |
13 | class MysqlDB(Database):
14 | class Result:
15 | def __init__(self, cur: SteadyDBCursor):
16 | self.res: list = cur.fetchall()
17 | self.lastrowid: int = cur.lastrowid
18 | self.rowcount: int = cur.rowcount
19 |
20 | def fetchall(self):
21 | return self.res
22 |
23 | def fetchone(self):
24 | return self.res[0]
25 |
26 | def __iter__(self):
27 | return self.res.__iter__()
28 |
29 | class Connection:
30 | def __init__(self, conn):
31 | self.conn = conn
32 | self.cur = conn.cursor()
33 |
34 | def get_cursor(self):
35 | return self.cur
36 |
37 | def commit(self):
38 | self.conn.commit()
39 |
40 | def rollback(self):
41 | self.conn.rollback()
42 |
43 | def close(self):
44 | self.cur.close()
45 | self.conn.close()
46 |
47 |
48 | def __init__(self,
49 | host: Optional[str],
50 | name: Optional[str],
51 | passwd: Optional[str],
52 | port: Optional[str],
53 | database: str = "HBlog"):
54 | if host is None or name is None:
55 | raise DBException
56 |
57 | super(MysqlDB, self).__init__(host=host, name=name, passwd=passwd, port=port)
58 | self.database = database
59 |
60 | self.pool = PooledDB(pymysql,
61 | mincached=1,
62 | maxcached=4,
63 | maxconnections=16,
64 | blocking=True,
65 | host=self._host,
66 | port=self._port,
67 | user=self._name,
68 | passwd=self._passwd,
69 | db=self.database)
70 |
71 | self.logger.info(f"MySQL({self._name}@{self._host}) connect")
72 |
73 | def get_connection(self):
74 | return MysqlDB.Connection(self.pool.connection())
75 |
76 | def search(self, sql: str, *args) -> Union[None, Result]:
77 | return self.__search(sql, args)
78 |
79 | def insert(self, sql: str, *args, connection: Connection = None) -> Union[None, Result]:
80 | return self.__done(sql, args, connection)
81 |
82 | def delete(self, sql: str, *args, connection: Connection = None) -> Union[None, Result]:
83 | return self.__done(sql, args, connection)
84 |
85 | def update(self, sql: str, *args, connection: Connection = None) -> Union[None, Result]:
86 | return self.__done(sql, args, connection)
87 |
88 | def __search(self, sql, args) -> Union[None, Result]:
89 | conn = self.pool.connection()
90 | cur = conn.cursor()
91 |
92 | try:
93 | cur.execute(query=sql, args=args)
94 | except pymysql.MySQLError:
95 | self.logger.error(f"MySQL({self._name}@{self._host}) SQL {sql} with {args} error {inspect.stack()[2][2]} "
96 | f"{inspect.stack()[2][1]} {inspect.stack()[2][3]}", exc_info=True, stack_info=True)
97 | return None
98 | else:
99 | return MysqlDB.Result(cur)
100 | finally:
101 | cur.close()
102 | conn.close()
103 |
104 | def __done(self, sql, args, connection: Connection = None) -> Union[None, Result]:
105 | if connection:
106 | cur = connection.get_cursor()
107 | conn = None
108 | else:
109 | conn = self.pool.connection()
110 | cur = conn.cursor()
111 |
112 | try:
113 | cur.execute(query=sql, args=args)
114 | if conn:
115 | conn.commit()
116 | except pymysql.MySQLError:
117 | if conn:
118 | conn.rollback()
119 | self.logger.error(f"MySQL({self._name}@{self._host}) SQL {sql} error {inspect.stack()[2][2]} "
120 | f"{inspect.stack()[2][1]} {inspect.stack()[2][3]}", exc_info=True, stack_info=True)
121 | return None
122 | else:
123 | return MysqlDB.Result(cur)
124 | finally:
125 | if not connection:
126 | cur.close()
127 | conn.close()
128 |
--------------------------------------------------------------------------------
/object/user.py:
--------------------------------------------------------------------------------
1 | from flask_login import UserMixin, AnonymousUserMixin
2 | from werkzeug.security import generate_password_hash, check_password_hash
3 | from itsdangerous import URLSafeTimedSerializer as Serializer
4 | from itsdangerous.exc import BadData
5 | from collections import namedtuple
6 |
7 | from configure import conf
8 | from sql.user import (read_user,
9 | check_role,
10 | create_user,
11 | get_role_name,
12 | delete_user,
13 | change_passwd_hash,
14 | create_role,
15 | delete_role,
16 | set_user_role,
17 | get_role_list,
18 | role_authority,
19 | get_user_email)
20 | import object.blog
21 | import object.comment
22 | import object.msg
23 |
24 |
25 | class AnonymousUser(AnonymousUserMixin):
26 | def __init__(self):
27 | super(AnonymousUser, self).__init__()
28 | self.role = 4 # 默认角色
29 | self.email = "" # 无邮箱
30 | self.passwd_hash = "" # 无密码
31 |
32 | def check_role(self, operate: str):
33 | return check_role(self.role, operate)
34 |
35 | @property
36 | def id(self):
37 | return 0
38 |
39 |
40 | class _User(UserMixin):
41 | user_tuple = namedtuple("User", "passwd role id")
42 |
43 | @staticmethod
44 | def create(email, passwd_hash):
45 | if create_user(email, passwd_hash) is not None:
46 | return User(email)
47 | return None
48 |
49 | @staticmethod
50 | def creat_token(email: str, passwd_hash: str):
51 | s = Serializer(conf["SECRET_KEY"])
52 | return s.dumps({"email": email, "passwd_hash": passwd_hash})
53 |
54 | @staticmethod
55 | def load_token(token: str):
56 | s = Serializer(conf["SECRET_KEY"])
57 | try:
58 | token = s.loads(token, max_age=3600)
59 | return token['email'], token['passwd_hash']
60 | except BadData:
61 | return None
62 |
63 | @staticmethod
64 | def get_passwd_hash(passwd: str):
65 | return generate_password_hash(passwd)
66 |
67 | @staticmethod
68 | def create_role(name: str, authority):
69 | return create_role(name, authority)
70 |
71 | @staticmethod
72 | def delete_role(role_id: int):
73 | return delete_role(role_id)
74 |
75 | @staticmethod
76 | def get_role_list():
77 | return get_role_list()
78 |
79 |
80 | class User(_User):
81 | RoleAuthorize = role_authority
82 |
83 | def __init__(self, email, is_id=False):
84 | if is_id:
85 | self.email = get_user_email(email)
86 | else:
87 | self.email = email
88 |
89 | def get_id(self):
90 | """Flask要求的方法"""
91 | return self.email
92 |
93 | @property
94 | def is_active(self):
95 | """Flask要求的属性, 表示用户是否激活(可登录), HGSSystem没有封禁用户系统, 所有用户都是被激活的"""
96 | return self.id != -1
97 |
98 | @property
99 | def is_authenticated(self):
100 | """Flask要求的属性, 表示登录的凭据是否正确, 这里检查是否能 load_user_by_id"""
101 | return self.is_active
102 |
103 | @property
104 | def star_email(self):
105 | if len(self.email) <= 4:
106 | return f"{self.email[0]}****"
107 | else:
108 | email = f"{self.email[0]}****{self.email[5:]}"
109 | return email
110 |
111 | @property
112 | def info(self):
113 | return User.user_tuple(*read_user(self.email))
114 |
115 | @property
116 | def passwd_hash(self):
117 | return self.info.passwd
118 |
119 | @property
120 | def role(self):
121 | return self.info.role
122 |
123 | @property
124 | def role_name(self):
125 | return get_role_name(self.info.role)
126 |
127 | @property
128 | def id(self):
129 | return self.info.id
130 |
131 | @property
132 | def count(self):
133 | msg = object.msg.Message.get_msg_count(self)
134 | comment = object.comment.Comment.get_user_comment_count(self)
135 | blog = object.blog.BlogArticle.get_blog_count(None, self)
136 | return msg, comment, blog
137 |
138 | def check_passwd(self, passwd: str):
139 | return check_password_hash(self.passwd_hash, passwd)
140 |
141 | def check_role(self, operate: str):
142 | return check_role(self.role, operate)
143 |
144 | def delete(self):
145 | return delete_user(self.id)
146 |
147 | def change_passwd(self, passwd):
148 | return change_passwd_hash(self.email, self.get_passwd_hash(passwd))
149 |
150 | def set_user_role(self, role_id: int):
151 | return set_user_role(role_id, self.id)
152 |
--------------------------------------------------------------------------------
/templates/docx/docx.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %} 博客 {% endblock %}
4 |
5 | {% block style %}
6 | {{ super() }}
7 |
8 |
9 |
10 | {% endblock %}
11 |
12 | {% block content %}
13 |
14 | {% if form and current_user.check_role("WriteBlog") %}
15 | {# 判断是否有权限写博客 #}
16 |
55 |
56 | {% endif %}
57 |
58 | {% if current_user.check_role("ReadBlog") %}
59 | {# 检查是否具有读取权限 #}
60 | {% cache conf["LIST_CACHE_EXPIRE"], ":blog", ":page", cache_str %}
61 |
62 |
63 | {% for blog in blog_list %}
64 | {% if blog.top %}
65 | {{ render_docx_top(blog, show_delete) }}
66 | {% else %}
67 | {{ render_docx(blog, show_delete) }}
68 | {% endif %}
69 | {% endfor %}
70 |
71 |
72 |
73 |
74 |
77 |
78 | {% endcache %}
79 |
80 | {% endif %}
81 |
82 | {% endblock %}
83 |
84 | {% block javascript %}
85 | {{ super() }}
86 |
87 | {% if form and current_user.check_role("WriteBlog") %}
88 |
89 |
90 |
109 | {% endif %}
110 | {% endblock %}
--------------------------------------------------------------------------------
/init.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS role -- 角色表
2 | (
3 | RoleID INT PRIMARY KEY AUTO_INCREMENT,
4 | RoleName char(20) NOT NULL UNIQUE,
5 | WriteBlog bit DEFAULT 0, -- 写博客
6 | WriteComment bit DEFAULT 1, -- 写评论
7 | WriteMsg bit DEFAULT 1, -- 写留言
8 | CreateUser bit DEFAULT 0, -- 创建新用户
9 |
10 | ReadBlog bit DEFAULT 1, -- 读博客
11 | ReadComment bit DEFAULT 1, -- 读评论
12 | ReadMsg bit DEFAULT 1, -- 读留言
13 | ReadSecretMsg bit DEFAULT 0, -- 读私密留言
14 | ReadUserInfo bit DEFAULT 0, -- 读取用户信息
15 |
16 | DeleteBlog bit DEFAULT 0, -- 删除博客
17 | DeleteComment bit DEFAULT 0, -- 删除评论
18 | DeleteMsg bit DEFAULT 0, -- 删除留言
19 | DeleteUser bit DEFAULT 0, -- 删除用户
20 |
21 | ConfigureSystem bit DEFAULT 0, -- 配置系统
22 | ReadSystem bit DEFAULT 0 -- 读系统信息
23 | ) CHARACTER SET utf8 COLLATE utf8_unicode_ci;
24 |
25 | CREATE TABLE IF NOT EXISTS user -- 创建用户表
26 | (
27 | ID INT PRIMARY KEY AUTO_INCREMENT,
28 | Email char(32) NOT NULL UNIQUE,
29 | PasswdHash char(128) NOT NULL,
30 | Role INT NOT NULL DEFAULT 3,
31 | FOREIGN KEY (Role) REFERENCES role (RoleID)
32 | ) CHARACTER SET utf8 COLLATE utf8_unicode_ci;
33 |
34 | INSERT INTO role (RoleID,
35 | RoleName,
36 | WriteBlog,
37 | WriteComment,
38 | WriteMsg,
39 | CreateUser,
40 | ReadBlog,
41 | ReadComment,
42 | ReadMsg,
43 | ReadSecretMsg,
44 | ReadUserInfo,
45 | DeleteBlog,
46 | DeleteComment,
47 | DeleteMsg,
48 | DeleteUser,
49 | ConfigureSystem,
50 | ReadSystem)
51 | VALUES (1, 'Admin', 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), -- 管理员用户
52 | (2, 'Coordinator', 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1); -- 协管员用户
53 |
54 | INSERT INTO role (RoleID, RoleName)
55 | VALUES (3, 'Default'); -- 默认用户
56 |
57 | INSERT INTO role (RoleID, RoleName, WriteComment, WriteMsg)
58 | VALUES (4, 'Anonymous', 0, 0); -- 默认用户
59 |
60 | CREATE TABLE IF NOT EXISTS blog -- 创建博客表
61 | (
62 | ID INT PRIMARY KEY AUTO_INCREMENT, -- 文章 ID
63 | Auth INT NOT NULL, -- 作者
64 | Title char(20) NOT NULL, -- 标题
65 | SubTitle char(20) NOT NULL, -- 副标题
66 | Content TEXT NOT NULL, -- 内容
67 | CreateTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建的时间
68 | UpdateTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建的时间
69 | Top BIT NOT NULL DEFAULT 0, -- 置顶
70 | FOREIGN KEY (Auth) REFERENCES user (ID)
71 | ) CHARACTER SET utf8 COLLATE utf8_unicode_ci;
72 |
73 | CREATE TABLE IF NOT EXISTS archive -- 归档表
74 | (
75 | ID INT PRIMARY KEY AUTO_INCREMENT, -- 归档 ID
76 | Name CHAR(30) NOT NULL UNIQUE, -- 归档名称
77 | DescribeText char(100) NOT NULL -- 描述
78 | ) CHARACTER SET utf8 COLLATE utf8_unicode_ci;
79 |
80 | CREATE TABLE IF NOT EXISTS blog_archive -- 归档表
81 | (
82 | BlogID INT, -- 文章ID
83 | ArchiveID INT, -- 归档ID
84 | FOREIGN KEY (BlogID) REFERENCES blog (ID),
85 | FOREIGN KEY (ArchiveID) REFERENCES archive (ID)
86 | ) CHARACTER SET utf8 COLLATE utf8_unicode_ci;
87 |
88 | CREATE VIEW blog_archive_with_name AS
89 | SELECT BlogID, ArchiveID, archive.Name As ArchiveName, archive.DescribeText AS DescribeText
90 | FROM blog_archive
91 | LEFT JOIN archive on blog_archive.ArchiveID = archive.ID;
92 |
93 | CREATE VIEW blog_with_archive AS
94 | SELECT blog.ID AS BlogID,
95 | blog_archive.ArchiveID AS ArchiveID,
96 | Auth,
97 | Title,
98 | SubTitle,
99 | Content,
100 | CreateTime,
101 | UpdateTime,
102 | Top
103 | FROM blog
104 | RIGHT JOIN blog_archive ON blog.ID = blog_archive.BlogID;
105 |
106 | CREATE TABLE IF NOT EXISTS comment -- 评论表
107 | (
108 | ID INT PRIMARY KEY AUTO_INCREMENT, -- 评论 ID
109 | BlogID INT NOT NULL, -- 博客 ID
110 | Auth INT NOT NULL, -- 作者
111 | Content TEXT NOT NULL, -- 内容
112 | CreateTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建的时间
113 | UpdateTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建的时间
114 | FOREIGN KEY (BlogID) REFERENCES blog (ID),
115 | FOREIGN KEY (Auth) REFERENCES user (ID)
116 | ) CHARACTER SET utf8 COLLATE utf8_unicode_ci;
117 |
118 | CREATE VIEW comment_user AS
119 | SELECT comment.ID as CommentID, BlogID, Auth, user.Email as Email, Content, CreateTime, UpdateTime
120 | FROM comment
121 | LEFT JOIN user on user.ID = comment.Auth
122 | ORDER BY UpdateTime DESC;
123 |
124 | CREATE TABLE IF NOT EXISTS message -- 留言表
125 | (
126 | ID INT PRIMARY KEY AUTO_INCREMENT, -- 留言 ID
127 | Auth INT NOT NULL, -- 作者
128 | Content TEXT NOT NULL, -- 内容
129 | Secret BIT NOT NULL DEFAULT 0, -- 私密内容
130 | CreateTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建的时间
131 | UpdateTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建的时间
132 | FOREIGN KEY (Auth) REFERENCES user (ID)
133 | ) CHARACTER SET utf8 COLLATE utf8_unicode_ci;
134 |
135 |
136 | CREATE VIEW message_user AS
137 | SELECT message.ID as MsgID, Auth, user.Email as Email, Content, CreateTime, UpdateTime, Secret
138 | FROM message
139 | LEFT JOIN user on user.ID = message.Auth
140 | ORDER BY UpdateTime DESC;
141 |
--------------------------------------------------------------------------------
/templates/auth/role.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %} 角色管理 {% endblock %}
4 |
5 | {% block style %}
6 | {{ super() }}
7 |
8 | {% endblock %}
9 |
10 | {% block content %}
11 |
12 |
13 |
14 |
15 | -
16 | 创建角色
17 |
18 | -
19 | 删除角色
20 |
21 | -
22 | 设置角色
23 |
24 |
25 |
26 |
27 |
28 |
58 |
59 |
88 |
89 |
119 |
120 |
121 |
122 |
123 |
124 | {% endblock %}
125 |
--------------------------------------------------------------------------------
/sql/user.py:
--------------------------------------------------------------------------------
1 | from sql import db, DB
2 | from sql.base import DBBit
3 | from sql.cache import (get_user_from_cache, write_user_to_cache, delete_user_from_cache,
4 | get_user_email_from_cache, write_user_email_to_cache, delete_user_email_from_cache,
5 | get_role_name_from_cache, write_role_name_to_cache, delete_role_name_from_cache,
6 | get_role_operate_from_cache, write_role_operate_to_cache, delete_role_operate_from_cache)
7 | import object.user
8 |
9 | from typing import List
10 |
11 | role_authority = ["WriteBlog", "WriteComment", "WriteMsg", "CreateUser",
12 | "ReadBlog", "ReadComment", "ReadMsg", "ReadSecretMsg", "ReadUserInfo",
13 | "DeleteBlog", "DeleteComment", "DeleteMsg", "DeleteUser",
14 | "ConfigureSystem", "ReadSystem"]
15 |
16 |
17 | def read_user(email: str, mysql: DB = db, not_cache=False):
18 | """ 读取用户 """
19 | if not not_cache:
20 | res = get_user_from_cache(email)
21 | if res is not None:
22 | return res
23 |
24 | cur = mysql.search("SELECT PasswdHash, Role, ID FROM user WHERE Email=%s", email)
25 | if cur is None or cur.rowcount != 1:
26 | return ["", -1, -1]
27 |
28 | res = cur.fetchone()
29 | write_user_to_cache(email, *res)
30 | return res
31 |
32 |
33 | def create_user(email: str, passwd: str, mysql: DB = db):
34 | """ 创建用户 """
35 | if len(email) == 0:
36 | return None
37 |
38 | cur = mysql.search("SELECT COUNT(*) FROM user")
39 | passwd = object.user.User.get_passwd_hash(passwd)
40 | if cur is None or cur.rowcount == 0 or cur.fetchone()[0] == 0:
41 | # 创建为管理员用户
42 | cur = mysql.insert("INSERT INTO user(Email, PasswdHash, Role) "
43 | "VALUES (%s, %s, %s)", email, passwd, 1)
44 | else:
45 | cur = mysql.insert("INSERT INTO user(Email, PasswdHash) "
46 | "VALUES (%s, %s)", email, passwd)
47 | if cur is None or cur.rowcount != 1:
48 | return None
49 | read_user(email, mysql) # 刷新缓存
50 | return cur.lastrowid
51 |
52 |
53 | def delete_user(user_id: int, mysql: DB = db):
54 | """ 删除用户 """
55 | delete_user_from_cache(get_user_email(user_id))
56 | delete_user_email_from_cache(user_id)
57 |
58 | conn = mysql.get_connection()
59 | cur = mysql.delete("DELETE FROM message WHERE Auth=%s", user_id, connection=conn)
60 | if cur is None:
61 | conn.rollback()
62 | conn.close()
63 | return False
64 |
65 | cur = mysql.delete("DELETE FROM comment WHERE Auth=%s", user_id, connection=conn)
66 | if cur is None:
67 | conn.rollback()
68 | conn.close()
69 | return False
70 |
71 | cur = mysql.delete("DELETE FROM blog WHERE Auth=%s", user_id, connection=conn)
72 | if cur is None:
73 | conn.rollback()
74 | conn.close()
75 | return False
76 |
77 | cur = mysql.delete("DELETE FROM user WHERE ID=%s", user_id, connection=conn)
78 | if cur is None or cur.rowcount == 0:
79 | conn.rollback()
80 | conn.close()
81 | return False
82 |
83 | conn.commit()
84 | conn.close()
85 | return True
86 |
87 |
88 | def change_passwd_hash(user_email: str, passwd_hash: str, mysql: DB = db):
89 | delete_user_from_cache(user_email)
90 | cur = mysql.update("UPDATE user "
91 | "SET PasswdHash=%s "
92 | "WHERE Email=%s", passwd_hash, user_email)
93 | read_user(user_email, mysql) # 刷新缓存
94 | if cur is None or cur.rowcount == 0:
95 | return False
96 | return True
97 |
98 |
99 | def get_user_email(user_id, mysql: DB = db, not_cache=False):
100 | """ 获取用户邮箱 """
101 | if not not_cache:
102 | res = get_user_email_from_cache(user_id)
103 | if res is not None:
104 | return res
105 |
106 | cur = mysql.search("SELECT Email FROM user WHERE ID=%s", user_id)
107 | if cur is None or cur.rowcount == 0:
108 | return None
109 |
110 | res = cur.fetchone()[0]
111 | write_user_email_to_cache(user_id, res)
112 | return res
113 |
114 |
115 | def __authority_to_sql(authority):
116 | """ authority 转换为 Update语句, 不检查合法性 """
117 | sql = []
118 | args = []
119 | for i in authority:
120 | sql.append(f"{i}=%s")
121 | args.append(authority[i])
122 | return ",".join(sql), args
123 |
124 |
125 | def create_role(name: str, authority: List[str], mysql: DB = db):
126 | conn = mysql.get_connection()
127 | cur = mysql.insert("INSERT INTO role(RoleName) VALUES (%s)", name, connection=conn)
128 | if cur is None or cur.rowcount == 0:
129 | conn.rollback()
130 | conn.close()
131 | return False
132 |
133 | sql, args = __authority_to_sql({i: (1 if i in authority else 0) for i in role_authority})
134 | cur = mysql.update(f"UPDATE role "
135 | f"SET {sql} "
136 | f"WHERE RoleName=%s", *args, name, connection=conn)
137 | if cur is None or cur.rowcount == 0:
138 | conn.rollback()
139 | conn.close()
140 | return False
141 |
142 | conn.commit()
143 | conn.close()
144 | return True
145 |
146 |
147 | def delete_role(role_id: int, mysql: DB = db):
148 | delete_role_name_from_cache(role_id)
149 | delete_role_operate_from_cache(role_id)
150 |
151 | cur = mysql.delete("DELETE FROM role WHERE RoleID=%s", role_id)
152 | if cur is None or cur.rowcount == 0:
153 | return False
154 | return True
155 |
156 |
157 | def set_user_role(role_id: int, user_id: str, mysql: DB = db):
158 | cur = mysql.update("UPDATE user "
159 | "SET Role=%s "
160 | "WHERE ID=%s", role_id, user_id)
161 | if cur is None or cur.rowcount == 0:
162 | return False
163 | return True
164 |
165 |
166 | def get_role_name(role: int, mysql: DB = db, not_cache=False):
167 | """ 获取用户角色名称 """
168 | if not not_cache:
169 | res = get_role_name_from_cache(role)
170 | if res is not None:
171 | return res
172 |
173 | cur = mysql.search("SELECT RoleName FROM role WHERE RoleID=%s", role)
174 | if cur is None or cur.rowcount == 0:
175 | return None
176 |
177 | res = cur.fetchone()[0]
178 | write_role_name_to_cache(role, res)
179 | return res
180 |
181 |
182 | def __check_operate(operate):
183 | return operate in role_authority
184 |
185 |
186 | def check_role(role: int, operate: str, mysql: DB = db, not_cache=False):
187 | """ 检查角色权限(通过角色ID) """
188 | if not __check_operate(operate): # 检查, 防止SQL注入
189 | return False
190 |
191 | if not not_cache:
192 | res = get_role_operate_from_cache(role, operate)
193 | if res is not None:
194 | return res
195 |
196 | cur = mysql.search(f"SELECT {operate} FROM role WHERE RoleID=%s", role)
197 | if cur is None or cur.rowcount == 0:
198 | return False
199 |
200 | res = cur.fetchone()[0] == DBBit.BIT_1
201 | write_role_operate_to_cache(role, operate, res)
202 | return res
203 |
204 |
205 | def get_role_list(mysql: DB = db):
206 | """ 获取归档列表 """
207 | cur = mysql.search("SELECT RoleID, RoleName FROM role")
208 | if cur is None or cur.rowcount == 0:
209 | return []
210 | return cur.fetchall()
211 |
212 |
213 | def get_role_list_iter(mysql: DB = db):
214 | """ 获取归档列表 """
215 | cur = mysql.search("SELECT RoleID, RoleName FROM role")
216 | if cur is None or cur.rowcount == 0:
217 | return []
218 | return cur
219 |
220 |
221 | def get_user_list_iter(mysql: DB = db):
222 | """ 获取归档列表 """
223 | cur = mysql.search("SELECT ID FROM user")
224 | if cur is None or cur.rowcount == 0:
225 | return []
226 | return cur
227 |
--------------------------------------------------------------------------------
/sql/blog.py:
--------------------------------------------------------------------------------
1 | from sql import db, DB
2 | from sql.base import DBBit
3 | from sql.archive import add_blog_to_archive
4 | from sql.cache import (write_blog_to_cache, get_blog_from_cache, delete_blog_from_cache,
5 | write_blog_count_to_cache, get_blog_count_from_cache, delete_blog_count_from_cache,
6 | write_archive_blog_count_to_cache, get_archive_blog_count_from_cache,
7 | delete_all_archive_blog_count_from_cache, delete_archive_blog_count_from_cache,
8 | write_user_blog_count_to_cache, get_user_blog_count_from_cache,
9 | delete_all_user_blog_count_from_cache, delete_user_blog_count_from_cache,
10 | delete_blog_archive_from_cache)
11 | import object.archive
12 |
13 | from typing import Optional, List
14 |
15 |
16 | def create_blog(auth_id: int, title: str, subtitle: str, content: str,
17 | archive_list: List[object.archive.Archive], mysql: DB = db) -> bool:
18 | """ 写入新的blog """
19 | delete_blog_count_from_cache()
20 | delete_user_blog_count_from_cache(auth_id)
21 | # archive cache 在下面循环删除
22 |
23 | cur = mysql.insert("INSERT INTO blog(Auth, Title, SubTitle, Content) "
24 | "VALUES (%s, %s, %s, %s)", auth_id, title, subtitle, content)
25 | if cur is None or cur.rowcount == 0:
26 | return False
27 |
28 | blog_id = cur.lastrowid
29 | for archive in archive_list:
30 | if not add_blog_to_archive(blog_id, archive.id):
31 | return False
32 | delete_archive_blog_count_from_cache(archive.id)
33 | read_blog(blog_id, mysql) # 刷新缓存
34 | return True
35 |
36 |
37 | def update_blog(blog_id: int, content: str, mysql: DB = db) -> bool:
38 | """ 更新博客文章 """
39 | delete_blog_from_cache(blog_id)
40 |
41 | cur = mysql.update("Update blog "
42 | "SET UpdateTime=CURRENT_TIMESTAMP(), Content=%s "
43 | "WHERE ID=%s", content, blog_id)
44 | if cur is None or cur.rowcount != 1:
45 | return False
46 | read_blog(blog_id, mysql) # 刷新缓存
47 | return True
48 |
49 |
50 | def read_blog(blog_id: int, mysql: DB = db, not_cache=False) -> list:
51 | """ 读取blog内容 """
52 | if not not_cache:
53 | res = get_blog_from_cache(blog_id)
54 | if res is not None:
55 | return res
56 |
57 | cur = mysql.search("SELECT Auth, Title, SubTitle, Content, UpdateTime, CreateTime, Top "
58 | "FROM blog "
59 | "WHERE ID=%s", blog_id)
60 | if cur is None or cur.rowcount == 0:
61 | return [-1, "", "", "", 0, -1, False]
62 | res = cur.fetchone()
63 | write_blog_to_cache(blog_id, *res, is_db_bit=True)
64 | return [*res[:6], res[-1] == DBBit.BIT_1]
65 |
66 |
67 | def delete_blog(blog_id: int, mysql: DB = db):
68 | delete_blog_count_from_cache()
69 | delete_all_archive_blog_count_from_cache()
70 | delete_all_user_blog_count_from_cache()
71 | delete_blog_from_cache(blog_id)
72 | delete_blog_archive_from_cache(blog_id)
73 |
74 | conn = mysql.get_connection()
75 | cur = mysql.delete("DELETE FROM blog_archive WHERE BlogID=%s", blog_id, connection=conn)
76 | if cur is None:
77 | conn.rollback()
78 | conn.close()
79 | return False
80 |
81 | cur = mysql.delete("DELETE FROM comment WHERE BlogID=%s", blog_id, connection=conn)
82 | if cur is None:
83 | conn.rollback()
84 | conn.close()
85 | return False
86 |
87 | cur = mysql.delete("DELETE FROM blog WHERE ID=%s", blog_id, connection=conn)
88 | if cur is None or cur.rowcount == 0:
89 | conn.rollback()
90 | conn.close()
91 | return False
92 |
93 | conn.commit()
94 | conn.close()
95 | return True
96 |
97 |
98 | def set_blog_top(blog_id: int, top: bool = True, mysql: DB = db):
99 | delete_blog_from_cache(blog_id)
100 | cur = mysql.update("UPDATE blog "
101 | "SET Top=%s "
102 | "WHERE ID=%s", 1 if top else 0, blog_id)
103 | if cur is None or cur.rowcount != 1:
104 | return False
105 | read_blog(blog_id, mysql) # 刷新缓存
106 | return True
107 |
108 |
109 | def get_blog_list(limit: Optional[int] = None, offset: Optional[int] = None, mysql: DB = db) -> list:
110 | """ 获得 blog 列表 """
111 | if limit is not None and offset is not None:
112 | cur = mysql.search("SELECT ID "
113 | "FROM blog "
114 | "ORDER BY Top DESC, CreateTime DESC, Title, SubTitle "
115 | "LIMIT %s OFFSET %s", limit, offset)
116 | else:
117 | cur = mysql.search("SELECT ID "
118 | "FROM blog "
119 | "ORDER BY Top DESC, CreateTime DESC, Title, SubTitle")
120 | if cur is None or cur.rowcount == 0:
121 | return []
122 | return [i[0] for i in cur.fetchall()]
123 |
124 |
125 | def get_blog_list_iter(mysql: DB = db):
126 | """ 获得 blog 列表 """
127 | cur = mysql.search("SELECT ID "
128 | "FROM blog "
129 | "ORDER BY Top DESC, CreateTime DESC, Title, SubTitle")
130 | if cur is None or cur.rowcount == 0:
131 | return []
132 | return cur
133 |
134 |
135 | def get_blog_list_not_top(limit: Optional[int] = None, offset: Optional[int] = None, mysql: DB = db) -> list:
136 | """ 获得blog列表 忽略置顶 """
137 | if limit is not None and offset is not None:
138 | cur = mysql.search("SELECT ID "
139 | "FROM blog "
140 | "ORDER BY CreateTime DESC, Title, SubTitle "
141 | "LIMIT %s OFFSET %s", limit, offset)
142 | else:
143 | cur = mysql.search("SELECT ID "
144 | "FROM blog "
145 | "ORDER BY CreateTime DESC, Title, SubTitle")
146 | if cur is None or cur.rowcount == 0:
147 | return []
148 | return [i[0] for i in cur.fetchall()]
149 |
150 |
151 | def get_archive_blog_list(archive_id, limit: Optional[int] = None,
152 | offset: Optional[int] = None,
153 | mysql: DB = db) -> list:
154 | """ 获得指定归档的 blog 列表 """
155 | if limit is not None and offset is not None:
156 | cur = mysql.search("SELECT BlogID "
157 | "FROM blog_with_archive "
158 | "WHERE ArchiveID=%s "
159 | "ORDER BY Top DESC, CreateTime DESC, Title, SubTitle "
160 | "LIMIT %s OFFSET %s", archive_id, limit, offset)
161 | else:
162 | cur = mysql.search("SELECT BlogID "
163 | "FROM blog_with_archive "
164 | "WHERE ArchiveID=%s "
165 | "ORDER BY Top DESC, CreateTime DESC, Title, SubTitle")
166 | if cur is None or cur.rowcount == 0:
167 | return []
168 | return [i[0] for i in cur.fetchall()]
169 |
170 |
171 | def get_blog_count(mysql: DB = db, not_cache=False) -> int:
172 | """ 统计 blog 个数 """
173 | if not not_cache:
174 | res = get_blog_count_from_cache()
175 | if res is not None:
176 | return res
177 |
178 | cur = mysql.search("SELECT COUNT(*) FROM blog")
179 | if cur is None or cur.rowcount == 0:
180 | return 0
181 |
182 | res = cur.fetchone()[0]
183 | write_blog_count_to_cache(res)
184 | return res
185 |
186 |
187 | def get_archive_blog_count(archive_id, mysql: DB = db, not_cache=False) -> int:
188 | """ 统计指定归档的 blog 个数 """
189 | if not not_cache:
190 | res = get_archive_blog_count_from_cache(archive_id)
191 | if res is not None:
192 | return res
193 |
194 | cur = mysql.search("SELECT COUNT(*) FROM blog_with_archive WHERE ArchiveID=%s", archive_id)
195 | if cur is None or cur.rowcount == 0:
196 | return 0
197 |
198 | res = cur.fetchone()[0]
199 | write_archive_blog_count_to_cache(archive_id, res)
200 | return res
201 |
202 |
203 | def get_user_blog_count(user_id: int, mysql: DB = db, not_cache=False) -> int:
204 | """ 获得指定用户的 blog 个数 """
205 | if not not_cache:
206 | res = get_user_blog_count_from_cache(user_id)
207 | if res is not None:
208 | return res
209 |
210 | cur = mysql.search("SELECT COUNT(*) FROM blog WHERE Auth=%s", user_id)
211 | if cur is None or cur.rowcount == 0:
212 | return 0
213 |
214 | res = cur.fetchone()[0]
215 | write_user_blog_count_to_cache(user_id, res)
216 | return res
217 |
--------------------------------------------------------------------------------
/app/app.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | from flask import Flask, url_for, request, current_app, render_template, Response, jsonify
5 | from flask_mail import Mail
6 | from flask_login import LoginManager, current_user
7 | from flask_moment import Moment
8 | from flask.logging import default_handler
9 | from typing import Optional, Union
10 |
11 | import logging.handlers
12 | import logging
13 | from bs4 import BeautifulSoup
14 |
15 | from configure import conf
16 | from object.user import AnonymousUser, User
17 | from app.cache import cache
18 | from app.http_auth import http_auth
19 |
20 | if conf["DEBUG_PROFILE"]:
21 | from werkzeug.middleware.profiler import ProfilerMiddleware
22 |
23 |
24 | class HBlogFlask(Flask):
25 | def __init__(self, import_name: str, *args, **kwargs):
26 | super(HBlogFlask, self).__init__(import_name, *args, **kwargs)
27 | self.about_me = ""
28 | self.update_configure()
29 |
30 | if conf["DEBUG_PROFILE"]:
31 | self.wsgi_app = ProfilerMiddleware(self.wsgi_app, sort_by=("cumtime",))
32 |
33 | self.login_manager = LoginManager()
34 | self.login_manager.init_app(self)
35 | self.login_manager.anonymous_user = AnonymousUser # 设置未登录的匿名对象
36 | self.login_manager.login_view = "auth.login_page"
37 |
38 | self.mail = Mail(self)
39 | self.moment = Moment(self)
40 | self.cache = cache
41 | self.cache.init_app(self)
42 | self.http_auth = http_auth
43 |
44 | self.logger.removeHandler(default_handler)
45 | self.logger.setLevel(conf["LOG_LEVEL"])
46 | self.logger.propagate = False
47 | if len(conf["LOG_HOME"]) > 0:
48 | handle = logging.handlers.TimedRotatingFileHandler(
49 | os.path.join(conf["LOG_HOME"], f"flask.log"), backupCount=10)
50 | handle.setFormatter(logging.Formatter(conf["LOG_FORMAT"]))
51 | self.logger.addHandler(handle)
52 | if conf["LOG_STDERR"]:
53 | handle = logging.StreamHandler(sys.stderr)
54 | handle.setFormatter(logging.Formatter(conf["LOG_FORMAT"]))
55 | self.logger.addHandler(handle)
56 |
57 | @self.login_manager.user_loader
58 | def user_loader(email: str):
59 | user = User(email)
60 | if user.info.id == -1:
61 | return None
62 | return user
63 |
64 | for i in [400, 401, 403, 404, 405, 408, 410, 413, 414, 423, 500, 501, 502]:
65 | def create_error_handle(status):
66 | def error_handle(e):
67 | self.print_load_page_log(status)
68 | if "/api" in request.base_url:
69 | rsp = jsonify({"status": status, "error": str(e)})
70 | rsp.status_code = status
71 | return rsp
72 | data = render_template('error.html', error_code=status, error_info=e)
73 | return Response(response=data, status=status)
74 | return error_handle
75 |
76 | self.errorhandler(i)(create_error_handle(i))
77 |
78 | def register_all_blueprint(self):
79 | import app.index as index
80 | import app.archive as archive
81 | import app.docx as docx
82 | import app.msg as msg
83 | import app.oss as oss
84 | import app.auth as auth
85 | import app.about_me as about_me
86 | import app.api as api
87 |
88 | self.register_blueprint(index.index, url_prefix="/")
89 | self.register_blueprint(archive.archive, url_prefix="/archive")
90 | self.register_blueprint(docx.docx, url_prefix="/docx")
91 | self.register_blueprint(msg.msg, url_prefix="/msg")
92 | self.register_blueprint(auth.auth, url_prefix="/auth")
93 | self.register_blueprint(about_me.about_me, url_prefix="/about")
94 | self.register_blueprint(oss.oss, url_prefix="/oss")
95 | self.register_blueprint(api.api, url_prefix="/api")
96 |
97 | def update_configure(self):
98 | """ 更新配置 """
99 | self.config.update(conf)
100 | about_me_page = conf["ABOUT_ME_PAGE"]
101 | if len(about_me_page) > 0 and os.path.exists(about_me_page):
102 | with open(about_me_page, "r", encoding='utf-8') as f:
103 | bs = BeautifulSoup(f.read(), "html.parser")
104 | self.about_me = str(bs.find("body").find("div", class_="about-me")) # 提取about-me部分的内容
105 |
106 | @staticmethod
107 | def get_max_page(count: int, count_page: int):
108 | """ 计算页码数 (共计count个元素, 每页count_page个元素) """
109 | return (count // count_page) + (0 if count % count_page == 0 else 1)
110 |
111 | @staticmethod
112 | def get_page(url, page: int, count: int):
113 | """ 计算页码的按钮 """
114 | if count <= 9:
115 | page_list = [[i + 1, url_for(url, page=i + 1)] for i in range(count)]
116 | elif page <= 5:
117 | """
118 | [1][2][3][4][5][6][...][count - 1][count]
119 | """
120 | page_list = [[i + 1, url_for(url, page=i + 1)] for i in range(6)]
121 | page_list += [None, [count - 1, url_for(url, page=count - 1)], [count, url_for(url, page=count)]]
122 | elif page >= count - 5:
123 | """
124 | [1][2][...][count - 5][count - 4][count - 3][count - 2][count - 1][count]
125 | """
126 | page_list: Optional[list] = [[1, url_for(url, page=1)], [2, url_for(url, page=2)], None]
127 | page_list += [[count - 5 + i, url_for(url, page=count - 5 + i)] for i in range(6)]
128 | else:
129 | """
130 | [1][2][...][page - 2][page - 1][page][page + 1][page + 2][...][count - 1][count]
131 | """
132 | page_list: Optional[list] = [[1, url_for(url, page=1)], [2, url_for(url, page=2)], None]
133 | page_list += [[page - 2 + i, url_for(url, page=page - 2 + i)] for i in range(5)]
134 | page_list += [None, [count - 1, url_for(url, page=count - 1)], [count, url_for(url, page=count)]]
135 | return page_list
136 |
137 | @staticmethod
138 | def __get_log_request_info():
139 | return (f"user: '{current_user.email}' "
140 | f"url: '{request.url}' blueprint: '{request.blueprint}' "
141 | f"args: {request.args} form: {request.form} "
142 | f"accept_encodings: '{request.accept_encodings}' "
143 | f"accept_charsets: '{request.accept_charsets}' "
144 | f"accept_mimetypes: '{request.accept_mimetypes}' "
145 | f"accept_languages: '{request.accept_languages}'")
146 |
147 | @staticmethod
148 | def print_load_page_log(page: str):
149 | current_app.logger.debug(
150 | f"[{request.method}] Load - '{page}' " + HBlogFlask.__get_log_request_info())
151 |
152 | @staticmethod
153 | def print_form_error_log(opt: str):
154 | current_app.logger.warning(
155 | f"[{request.method}] '{opt}' - Bad form " + HBlogFlask.__get_log_request_info())
156 |
157 | @staticmethod
158 | def print_sys_opt_fail_log(opt: str):
159 | current_app.logger.error(
160 | f"[{request.method}] System {opt} - fail " + HBlogFlask.__get_log_request_info())
161 |
162 | @staticmethod
163 | def print_sys_opt_success_log(opt: str):
164 | current_app.logger.warning(
165 | f"[{request.method}] System {opt} - success " + HBlogFlask.__get_log_request_info())
166 |
167 | @staticmethod
168 | def print_user_opt_fail_log(opt: str):
169 | current_app.logger.debug(
170 | f"[{request.method}] User {opt} - fail " + HBlogFlask.__get_log_request_info())
171 |
172 | @staticmethod
173 | def print_user_opt_success_log(opt: str):
174 | current_app.logger.debug(
175 | f"[{request.method}] User {opt} - success " + HBlogFlask.__get_log_request_info())
176 |
177 | @staticmethod
178 | def print_user_opt_error_log(opt: str):
179 | current_app.logger.warning(
180 | f"[{request.method}] User {opt} - system fail " + HBlogFlask.__get_log_request_info())
181 |
182 | @staticmethod
183 | def print_import_user_opt_success_log(opt: str):
184 | current_app.logger.info(
185 | f"[{request.method}] User {opt} - success " + HBlogFlask.__get_log_request_info())
186 |
187 | @staticmethod
188 | def print_user_not_allow_opt_log(opt: str):
189 | current_app.logger.info(
190 | f"[{request.method}] User '{opt}' - reject " + HBlogFlask.__get_log_request_info())
191 |
192 |
193 | Hblog = Union[HBlogFlask, Flask]
194 |
--------------------------------------------------------------------------------
/app/api.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify, request, abort, g
2 | from configure import conf
3 | from json import loads
4 | from datetime import datetime
5 |
6 | from object.archive import Archive
7 | from object.blog import BlogArticle
8 | from object.msg import Message
9 | from object.comment import Comment
10 | from object.user import User
11 |
12 | from app.http_auth import http_auth
13 | from app.tool import api_role_required
14 |
15 |
16 | api = Blueprint("api", __name__)
17 |
18 |
19 | @api.route("/", methods=["GET", "POST"])
20 | def api_say_hello():
21 | json = loads(request.get_json())
22 | name = "unknown"
23 | if json:
24 | name = json.get("name", "unknown")
25 | res = {"status": 200, name: "Hello!"}
26 | return res
27 |
28 |
29 | @api.route("/get_introduce")
30 | @http_auth.login_required
31 | def api_get_introduce():
32 | title = request.args.get("title", "", type=str).lower()
33 |
34 | res = {"status": 200, "introduce": {}}
35 | have_found = False
36 | for info in conf['INTRODUCE']:
37 | if title is None or title == info[0].lower():
38 | res["introduce"][info[0]] = info[1]
39 | have_found = True
40 |
41 | if not have_found:
42 | abort(404)
43 | return jsonify(res)
44 |
45 |
46 | @api.route("/find_me")
47 | @http_auth.login_required
48 | def api_get_find_me():
49 | where = request.args.get("where", None, type=str)
50 | if where:
51 | where = where.lower()
52 |
53 | res = {"status": 200, "content": {}}
54 | have_found = False
55 | for i in conf['INTRODUCE_LINK']:
56 | if where is None or where == i.lower():
57 | res["content"][i] = conf['INTRODUCE_LINK'][i]
58 | have_found = True
59 |
60 | if not have_found:
61 | abort(404)
62 | return jsonify(res)
63 |
64 |
65 | @api.route("/archive_list")
66 | @http_auth.login_required
67 | @api_role_required("ReadBlog", "api get archive list")
68 | def api_get_archive_list():
69 | archive_list = Archive.get_archive_list()
70 | res = {"status": 200}
71 | res_list = []
72 | for i in archive_list:
73 | res_list.append({
74 | "name": i.name,
75 | "describe": i.describe,
76 | "count": i.count,
77 | "id": i.id,
78 | })
79 |
80 | res["archive"] = res_list
81 | return jsonify(res)
82 |
83 |
84 | @api.route("/archive/")
85 | @http_auth.login_required
86 | @api_role_required("ReadBlog", "api get archive")
87 | def get_get_archive(archive_id):
88 | archive = Archive(archive_id)
89 | if len(archive.name) == 0:
90 | abort(404)
91 | return {
92 | "status": 200,
93 | "archive": {
94 | "name": archive.name,
95 | "describe": archive.describe,
96 | "count": archive.count,
97 | "id": archive.id,
98 | }
99 | }
100 |
101 |
102 | @api.route("/archive_blog_list//")
103 | @http_auth.login_required
104 | @api_role_required("ReadBlog", "api get archive blog list")
105 | def api_get_archive_blog_list(archive_id: int, page: int):
106 | blog_list = BlogArticle.get_blog_list(archive_id=archive_id, limit=20, offset=(page - 1) * 20)
107 | res = {"status": 200}
108 | res_list = []
109 | for i in blog_list:
110 | res_list.append({
111 | "auth": i.user.id,
112 | "title": i.title,
113 | "subtitle": i.subtitle,
114 | "update_time": datetime.timestamp(i.update_time),
115 | "create_time": datetime.timestamp(i.create_time),
116 | "top": i.top,
117 | "id": i.id,
118 | })
119 |
120 | res["blog"] = res_list
121 | return jsonify(res)
122 |
123 |
124 | @api.route("/blog_list/")
125 | @http_auth.login_required
126 | @api_role_required("ReadBlog", "api get blog list")
127 | def api_get_blog_list(page: int):
128 | blog_list = BlogArticle.get_blog_list(limit=20, offset=(page - 1) * 20)
129 | res = {"status": 200}
130 | res_list = []
131 | for i in blog_list:
132 | res_list.append({
133 | "auth": i.user.id,
134 | "title": i.title,
135 | "subtitle": i.subtitle,
136 | "update_time": datetime.timestamp(i.update_time),
137 | "create_time": datetime.timestamp(i.create_time),
138 | "top": i.top,
139 | "id": i.id,
140 | })
141 |
142 | res["blog"] = res_list
143 | return jsonify(res)
144 |
145 |
146 |
147 | @api.route("/blog/")
148 | @http_auth.login_required
149 | @api_role_required("ReadBlog", "api get blog")
150 | def api_get_blog(blog_id: int):
151 | blog = BlogArticle(blog_id)
152 | return {
153 | "status": 200,
154 | "blog": {
155 | "auth": blog.user.id,
156 | "title": blog.title,
157 | "subtitle": blog.subtitle,
158 | "update_time": datetime.timestamp(blog.update_time),
159 | "create_time": datetime.timestamp(blog.create_time),
160 | "top": blog.top,
161 | "content": blog.content,
162 | "id": blog.id,
163 | }
164 | }
165 |
166 |
167 | @api.route("/get_blog_comment/")
168 | @http_auth.login_required
169 | @api_role_required("ReadComment", "api get blog comment")
170 | def api_get_blog_comment(blog_id: int):
171 | blog = BlogArticle(blog_id)
172 | res = {"status": 200}
173 | res_list = []
174 | for i in blog.comment:
175 | res_list.append({
176 | "auth": i.auth.id,
177 | "update_time": datetime.timestamp(i.update_time),
178 | "id": i.id,
179 | })
180 |
181 | res["comment"] = res_list
182 | return jsonify(res)
183 |
184 |
185 | @api.route("/comment/")
186 | @http_auth.login_required
187 | @api_role_required("ReadComment", "api get comment")
188 | def api_get_comment(comment_id: int):
189 | comment = Comment(comment_id)
190 | return {
191 | "status": 200,
192 | "blog": {
193 | "auth": comment.auth.id,
194 | "update_time": datetime.timestamp(comment.update_time),
195 | "content": comment.content,
196 | "id": comment.id,
197 | }
198 | }
199 |
200 |
201 | @api.route("/msg_list/")
202 | @http_auth.login_required
203 | @api_role_required("ReadMsg", "api get msg list")
204 | def api_get_not_secret_msg_list(page: int):
205 | msg_list = Message.get_message_list(20, (page - 1) * 20, False)
206 | res = {"status": 200}
207 | res_list = []
208 | for i in msg_list:
209 | res_list.append({
210 | "secret": i.secret,
211 | "auth": i.auth.id,
212 | "update_time": datetime.timestamp(i.update_time),
213 | "id": i.id,
214 | })
215 |
216 | res["blog"] = res_list
217 | return jsonify(res)
218 |
219 |
220 | @api.route("/s_msg_list/")
221 | @http_auth.login_required
222 | @api_role_required("ReadMsg", "api get all msg secret list")
223 | @api_role_required("ReadSecretMsg", "api get all secret list")
224 | def api_get_secret_msg_list(page: int):
225 | msg_list = Message.get_message_list(20, (page - 1) * 20, request.args.get("secret", 1, type=int) != 0)
226 | res = {"status": 200}
227 | res_list = []
228 | for i in msg_list:
229 | res_list.append({
230 | "secret": i.secret,
231 | "auth": i.auth.id,
232 | "update_time": datetime.timestamp(i.update_time),
233 | "id": i.id,
234 | })
235 |
236 | res["blog"] = res_list
237 | return jsonify(res)
238 |
239 |
240 | @api.route("/msg/")
241 | @http_auth.login_required
242 | @api_role_required("ReadMsg", "api get msg")
243 | def api_get_msg(msg_id: int):
244 | msg = Message(msg_id)
245 | if msg.secret:
246 | abort(404)
247 | return {
248 | "status": 200,
249 | "blog": {
250 | "auth": msg.auth.id,
251 | "update_time": datetime.timestamp(msg.update_time),
252 | "content": msg.content,
253 | "id": msg.id,
254 | }
255 | }
256 |
257 |
258 | @api.route("/s_msg/")
259 | @http_auth.login_required
260 | @api_role_required("ReadMsg", "api get secret msg")
261 | @api_role_required("ReadSecretMsg", "api get secret msg")
262 | def api_get_secret_msg(msg_id: int):
263 | msg = Message(msg_id)
264 | return {
265 | "status": 200,
266 | "blog": {
267 | "auth": msg.auth.id,
268 | "update_time": datetime.timestamp(msg.update_time),
269 | "content": msg.content,
270 | "id": msg.id,
271 | }
272 | }
273 |
274 |
275 | @api.route("/user/")
276 | @http_auth.login_required
277 | @api_role_required("ReadUserInfo", "api get user info")
278 | def api_get_user(user_id: int):
279 | user = User(user_id, is_id=True)
280 | return {
281 | "status": 200,
282 | "blog": {
283 | "role": user.role,
284 | "email": user.email,
285 | "id": user.id,
286 | }
287 | }
288 |
289 |
--------------------------------------------------------------------------------
/templates/docx/article.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %} 文档 {% endblock %}
4 |
5 | {% block style %}
6 | {{ super() }}
7 |
8 |
9 |
10 | {% endblock %}
11 |
12 | {% block content %}
13 |
14 | {% if current_user.check_role("ReadBlog") %}
15 | {# 检查是否具有读取权限 #}
16 |
17 |
18 | {{ article.title }} {{ article.subtitle }} {{ moment(article.update_time).format('YYYY-MM-DD HH:mm:ss') }} / {{ moment(article.create_time).format('YYYY-MM-DD HH:mm:ss') }}
19 | 点击量: {{ article.clicks }}
20 |
21 | {% for archive in article.archive %}
22 | {{ archive.name }}
23 | {% endfor %}
24 | 下载
25 |
26 |
27 |
58 | {% if can_update %}
59 |
80 |
81 |
82 |
83 |
更新博文
84 | {% if article.top %}
85 |
取消置顶
86 | {% else %}
87 |
置顶文章
88 | {% endif %}
89 |
更新归档
90 |
91 |
92 | {% endif %}
93 |
94 |
95 | {% endif %}
96 |
97 |
140 |
141 | {% endblock %}
142 |
143 | {% block javascript %}
144 | {{ super() }}
145 |
146 |
147 |
148 |
149 |
178 | {% endblock %}
--------------------------------------------------------------------------------
/templates/base.html:
--------------------------------------------------------------------------------
1 | {% import "macro.html" as macro %}
2 |
3 | {% macro render_docx_color(blog, color, show_delete) %}
4 | {# 使用到moment, 不能放进macro #}
5 | {% if show_delete %}
6 |
7 |
8 |
9 |
12 |
13 |
是否确认删除博文 {{ blog.title }}?
14 |
15 |
20 |
21 |
22 |
23 | {% endif %}
24 |
25 |
26 |
33 |
34 | Update Date: {{ moment(blog.update_time).format('YYYY-MM-DD HH:mm:ss') }}
35 |
36 | Create Date: {{ moment(blog.create_time).format('YYYY-MM-DD HH:mm:ss') }}
37 |
38 |
39 | {% if show_delete %}
40 |
删除
42 | {% endif %}
43 |
44 |
前往
45 |
46 |
47 |
48 | {% endmacro %}
49 |
50 | {% macro render_docx(blog, show_delete) %}
51 | {# 使用到moment, 不能放进macro #}
52 | {{ render_docx_color(blog, "bg-primary", show_delete) }}
53 | {% endmacro %}
54 |
55 | {% macro render_docx_top(blog, show_delete) %}
56 | {# 使用到moment, 不能放进macro #}
57 | {{ render_docx_color(blog, "bg-dark", show_delete) }}
58 | {% endmacro %}
59 |
60 | {% macro render_msg(msg, shod_delete)%}
61 | {# 使用到moment, 不能放进macro #}
62 | {% if show_delete %}
63 |
80 | {% endif %}
81 |
82 |
83 |
90 |
91 |
{{ msg.content.replace('\n', '
') | safe }}
92 |
93 | {% if show_delete %}
94 |
删除
96 | {% endif %}
97 |
98 | {% if msg.secret %}
99 |
[私]
100 | {% endif %}
101 |
102 |
{{ moment(msg.update_time).fromNow(refresh=True) }}
103 |
104 |
105 |
106 | {% endmacro %}
107 |
108 | {% macro render_comment(comment, shod_delete)%}
109 | {# 使用到moment, 不能放进macro #}
110 | {% if show_delete %}
111 |
128 | {% endif %}
129 |
130 | {# 此处使用上下间距 #}
131 |
138 |
139 |
{{ comment.content.replace('\n', '
') | safe }}
140 |
141 | {% if show_delete %}
142 |
删除
144 | {% endif %}
145 |
{{ moment(comment.update_time).fromNow(refresh=True) }}
146 |
147 |
148 |
149 | {% endmacro %}
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 | {% block icon %}
158 |
159 | {% endblock %}
160 |
161 | {% block font %}
162 |
163 |
164 |
165 | {% endblock %}
166 |
167 | {% block style %}
168 |
169 |
170 |
178 | {% endblock %}
179 |
180 | {% block javascript %}
181 |
182 | {{ moment.include_moment() }}
183 | {{ moment.lang("zh-CN") }}
184 | {% endblock %}
185 |
186 | {% block title %} {% endblock %} - {{ blog_name }}
187 |
188 |
189 | {% block nav %}
190 | 《{{ blog_name }}》—— {{ blog_describe }}
191 |
192 |
250 | {% endblock %}
251 |
252 |
253 | {% block content %} {% endblock %}
254 |
255 |
256 | {% block footer %}
257 | {# footer 最后加载 #}
258 |
275 |
299 | {% endblock %}
300 |
301 | {% block javascript_foot %} {% endblock %}
302 |
303 |
304 |
--------------------------------------------------------------------------------
/app/auth.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, render_template, redirect, flash, url_for, request, abort, current_app, g
2 | from flask_login import login_required, login_user, current_user, logout_user
3 | from flask_wtf import FlaskForm
4 | from wtforms import (EmailField,
5 | StringField,
6 | PasswordField,
7 | BooleanField,
8 | SelectMultipleField,
9 | SelectField,
10 | SubmitField,
11 | ValidationError)
12 | from wtforms.validators import DataRequired, Length, Regexp, EqualTo
13 | from urllib.parse import urljoin
14 |
15 | import app
16 | from object.user import User
17 | from send_email import send_msg
18 | from configure import conf
19 |
20 | auth = Blueprint("auth", __name__)
21 |
22 |
23 | class AuthField(FlaskForm):
24 | @staticmethod
25 | def email_field(name: str, description: str):
26 | """ 提前定义 email 字段的生成函数,供下文调用 """
27 | return EmailField(name, description=description,
28 | validators=[
29 | DataRequired(f"必须填写{name}"),
30 | Length(1, 32, message=f"{name}长度1-32个字符"),
31 | Regexp(r"^[a-zA-Z0-9_\.\-]+@[a-zA-Z0-9_\-]+(\.[a-zA-Z0-9_\.]+)+$",
32 | message=f"{name}不满足正则表达式")])
33 |
34 | @staticmethod
35 | def passwd_field(name: str, description: str):
36 | """ 提前定义 passwd 字段的生成函数,供下文调用 """
37 | return PasswordField(name, description=description,
38 | validators=[
39 | DataRequired(f"必须填写{name}"),
40 | Length(8, 32, message=f"{name}长度为8-32位")])
41 |
42 | @staticmethod
43 | def passwd_again_field(name: str, description: str, passwd: str = "passwd"):
44 | """ 提前定义 passwd again 字段的生成函数,供下文调用 """
45 | return PasswordField(f"重复{name}", description=description,
46 | validators=[
47 | DataRequired(message=f"必须再次填写{name}"),
48 | EqualTo(passwd, message=f"两次输入的{name}不相同")])
49 |
50 |
51 | class EmailPasswd(AuthField):
52 | email = AuthField.email_field("邮箱", "用户邮箱")
53 | passwd = AuthField.passwd_field("密码", "用户密码")
54 |
55 |
56 | class LoginForm(EmailPasswd):
57 | remember = BooleanField("记住我")
58 | submit = SubmitField("登录")
59 |
60 |
61 | class RegisterForm(EmailPasswd):
62 | passwd_again = AuthField.passwd_again_field("密码", "用户密码")
63 | submit = SubmitField("注册")
64 |
65 | def validate_email(self, field):
66 | """ 检验email是否合法 """
67 | if User(field.data).info[2] != -1:
68 | raise ValidationError("邮箱已被注册")
69 |
70 |
71 | class ChangePasswdForm(AuthField):
72 | old_passwd = AuthField.passwd_field("旧密码", "用户原密码")
73 | passwd = AuthField.passwd_field("新密码", "用户新密码")
74 | passwd_again = AuthField.passwd_again_field("新密码", "用户新密码")
75 | submit = SubmitField("修改密码")
76 |
77 | def validate_passwd(self, field):
78 | """ 检验新旧密码是否相同 """
79 | if field.data == self.old_passwd.data:
80 | raise ValidationError("新旧密码不能相同")
81 |
82 |
83 | class DeleteUserForm(AuthField):
84 | email = AuthField.email_field("邮箱", "用户邮箱")
85 | submit = SubmitField("删除用户")
86 |
87 | def __init__(self):
88 | super(DeleteUserForm, self).__init__()
89 | self.email_user = None
90 |
91 | def validate_email(self, field):
92 | """ 检验用户是否存在 """
93 | if User(field.data).info[2] == -1:
94 | raise ValidationError("邮箱用户不存在")
95 |
96 |
97 | class CreateRoleForm(AuthField):
98 | name = StringField("角色名称", validators=[DataRequired()])
99 | authority = SelectMultipleField("权限", coerce=str, choices=User.RoleAuthorize)
100 | submit = SubmitField("创建角色")
101 |
102 |
103 | class RoleForm(AuthField):
104 | name = SelectField("角色名称", validators=[DataRequired()], coerce=int)
105 |
106 | def __init__(self):
107 | super(RoleForm, self).__init__()
108 | self.name_res = []
109 | self.name_choices = []
110 | for i in User.get_role_list():
111 | self.name_res.append(i[0])
112 | self.name_choices.append((i[0], i[1]))
113 | self.name.choices = self.name_choices
114 |
115 | def validate_name(self, field):
116 | """ 检验角色是否存在 """
117 | if field.data not in self.name_res:
118 | raise ValidationError("角色不存在")
119 |
120 |
121 | class DeleteRoleForm(RoleForm):
122 | submit = SubmitField("删除角色")
123 |
124 |
125 | class SetRoleForm(RoleForm):
126 | email = AuthField.email_field("邮箱", "用户邮箱")
127 | submit = SubmitField("设置角色")
128 |
129 | def __init__(self):
130 | super(SetRoleForm, self).__init__()
131 | self.email_user = None
132 |
133 | def validate_email(self, field):
134 | if User(field.data).info[2] == -1:
135 | raise ValidationError("邮箱用户不存在")
136 |
137 |
138 | @auth.route('/user/yours')
139 | @login_required
140 | def yours_page():
141 | msg_count, comment_count, blog_count = current_user.count
142 | app.HBlogFlask.print_load_page_log("user info")
143 | return render_template("auth/yours.html", msg_count=msg_count, comment_count=comment_count, blog_count=blog_count)
144 |
145 |
146 | @auth.route('/user/login', methods=["GET", "POST"])
147 | def login_page():
148 | if current_user.is_authenticated: # 用户已经成功登陆
149 | app.HBlogFlask.print_user_not_allow_opt_log("login")
150 | return redirect(url_for("auth.yours_page"))
151 |
152 | form = LoginForm()
153 | if form.validate_on_submit():
154 | user = User(form.email.data)
155 | if user.info[2] != -1 and user.check_passwd(form.passwd.data):
156 | login_user(user, form.remember.data)
157 | next_page = request.args.get("next", None, type=str)
158 | if next_page is None or not next_page.startswith('/'):
159 | next_page = url_for('base.index_page')
160 | flash("登陆成功")
161 | app.HBlogFlask.print_user_opt_success_log(f"login {form.email.data}")
162 | return redirect(next_page)
163 | flash("账号或密码错误")
164 | app.HBlogFlask.print_user_opt_fail_log(f"login {form.email.data}")
165 | return redirect(url_for("auth.login_page"))
166 | app.HBlogFlask.print_load_page_log("user login")
167 | return render_template("auth/login.html", form=form)
168 |
169 |
170 | @auth.route('/user/register', methods=["GET", "POST"])
171 | def register_page():
172 | if current_user.is_authenticated:
173 | app.HBlogFlask.print_user_not_allow_opt_log("register")
174 | return redirect(url_for("auth.yours_page"))
175 |
176 | form = RegisterForm()
177 | if form.validate_on_submit():
178 | token = User.creat_token(form.email.data, form.passwd.data)
179 | register_url = urljoin(request.host_url, url_for("auth.confirm_page", token=token))
180 | hblog: app.Hblog = current_app
181 | send_msg("注册确认", hblog.mail, form.email.data, "register", register_url=register_url)
182 | flash("注册提交成功, 请进入邮箱点击确认注册链接")
183 | app.HBlogFlask.print_import_user_opt_success_log(f"register {form.email.data}")
184 | return redirect(url_for("base.index_page"))
185 | app.HBlogFlask.print_load_page_log("user register")
186 | return render_template("auth/register.html", RegisterForm=form)
187 |
188 |
189 | @auth.route('/user/confirm')
190 | def confirm_page():
191 | token = request.args.get("token", None, type=str)
192 | if token is None:
193 | app.HBlogFlask.print_user_opt_fail_log(f"Confirm (bad token)")
194 | abort(404)
195 | return
196 |
197 | token = User.load_token(token)
198 | if token is None:
199 | app.HBlogFlask.print_user_opt_fail_log(f"Confirm (bad token)")
200 | abort(404)
201 | return
202 |
203 | if User(token[0]).info[2] != -1:
204 | app.HBlogFlask.print_user_opt_fail_log(f"Confirm (bad token)")
205 | abort(404)
206 | return
207 |
208 | User.create(token[0], token[1])
209 | current_app.logger.info(f"{token[0]} confirm success")
210 | app.HBlogFlask.print_import_user_opt_success_log(f"confirm {token[0]}")
211 | flash(f"用户{token[0]}认证完成")
212 | return redirect(url_for("base.index_page"))
213 |
214 |
215 | @auth.route('/user/logout')
216 | @login_required
217 | def logout_page():
218 | app.HBlogFlask.print_import_user_opt_success_log(f"logout")
219 | logout_user()
220 | flash("退出登录成功")
221 | return redirect(url_for("base.index_page"))
222 |
223 |
224 | @auth.route('/user/set/passwd', methods=['GET', 'POST'])
225 | @login_required
226 | def change_passwd_page():
227 | form = ChangePasswdForm()
228 | if form.validate_on_submit():
229 | if not current_user.check_passwd(form.old_passwd.data):
230 | app.HBlogFlask.print_user_opt_error_log(f"change passwd")
231 | flash("旧密码错误")
232 | elif current_user.change_passwd(form.passwd.data):
233 | app.HBlogFlask.print_user_opt_success_log(f"change passwd")
234 | flash("密码修改成功")
235 | logout_user()
236 | return redirect(url_for("auth.login_page"))
237 | else:
238 | app.HBlogFlask.print_user_opt_error_log(f"change passwd")
239 | flash("密码修改失败")
240 | return redirect(url_for("auth.change_passwd_page"))
241 | app.HBlogFlask.print_load_page_log("user change passwd")
242 | return render_template("auth/passwd.html", ChangePasswdForm=form)
243 |
244 |
245 | @auth.route('/user/delete', methods=['GET', 'POST'])
246 | @login_required
247 | @app.role_required("DeleteUser", "delete user")
248 | def delete_user_page():
249 | form = DeleteUserForm()
250 | if form.validate_on_submit():
251 | user = form.email_user
252 | if user.delete():
253 | app.HBlogFlask.print_sys_opt_success_log(f"{current_user.email} delete user {form.email.data} success")
254 | flash("用户删除成功")
255 | else:
256 | app.HBlogFlask.print_sys_opt_fail_log(f"{current_user.email} delete user {form.email.data} fail")
257 | flash("用户删除失败")
258 | return redirect(url_for("auth.delete_user_page"))
259 | app.HBlogFlask.print_load_page_log("delete user")
260 | return render_template("auth/delete.html", DeleteUserForm=form)
261 |
262 |
263 | @auth.route('/role', methods=['GET'])
264 | @login_required
265 | @app.role_required("ConfigureSystem", "load role setting")
266 | def role_page():
267 | app.HBlogFlask.print_load_page_log("role setting")
268 | return render_template("auth/role.html",
269 | CreateRoleForm=CreateRoleForm(),
270 | DeleteRoleForm=DeleteRoleForm(),
271 | SetRoleForm=SetRoleForm())
272 |
273 |
274 | @auth.route('/role/create', methods=['POST'])
275 | @login_required
276 | @app.form_required(CreateRoleForm, "create role")
277 | @app.role_required("ConfigureSystem", "create role")
278 | def role_create_page():
279 | form: CreateRoleForm = g.form
280 | name = form.name.data
281 | if User.create_role(name, form.authority.data):
282 | app.HBlogFlask.print_sys_opt_success_log(f"Create role success: {name}")
283 | flash("角色创建成功")
284 | else:
285 | app.HBlogFlask.print_sys_opt_success_log(f"Create role fail: {name}")
286 | flash("角色创建失败")
287 | return redirect(url_for("auth.role_page"))
288 |
289 |
290 | @auth.route('/role/delete', methods=['POST'])
291 | @login_required
292 | @app.form_required(DeleteRoleForm, "delete role")
293 | @app.role_required("ConfigureSystem", "delete role")
294 | def role_delete_page():
295 | form: DeleteRoleForm = g.form
296 | if User.delete_role(form.name.data):
297 | app.HBlogFlask.print_sys_opt_success_log(f"Delete role success: {form.name.data}")
298 | flash("角色删除成功")
299 | else:
300 | app.HBlogFlask.print_sys_opt_fail_log(f"Delete role fail: {form.name.data}")
301 | flash("角色删除失败")
302 | return redirect(url_for("auth.role_page"))
303 |
304 |
305 | @auth.route('/role/set', methods=['POST'])
306 | @login_required
307 | @app.form_required(SetRoleForm, "assign user a role")
308 | @app.role_required("ConfigureSystem", "assign user a role")
309 | def role_set_page():
310 | form: SetRoleForm = g.form
311 | user = form.email_user
312 | if user.set_user_role(form.name.data):
313 | app.HBlogFlask.print_sys_opt_success_log(f"Role assign {form.email.data} -> {form.name.data}")
314 | flash("角色设置成功")
315 | else:
316 | app.HBlogFlask.print_sys_opt_fail_log(f"Role assign {form.email.data} -> {form.name.data}")
317 | flash("角色设置失败")
318 | return redirect(url_for("auth.role_page"))
319 |
320 |
321 | @auth.context_processor
322 | @app.cache.cached(timeout=conf["CACHE_EXPIRE"], key_prefix="inject_base:auth")
323 | def inject_base():
324 | """ auth 默认模板变量 """
325 | return {"top_nav": ["", "", "", "", "", "active"]}
326 |
--------------------------------------------------------------------------------
/sql/cache.py:
--------------------------------------------------------------------------------
1 | from sql import cache, DB
2 | from sql.base import DBBit
3 | from configure import conf
4 |
5 | from redis import RedisError
6 | from functools import wraps
7 | from datetime import datetime
8 |
9 | CACHE_TIME = int(conf["CACHE_EXPIRE"])
10 | CACHE_PREFIX = conf["CACHE_PREFIX"]
11 |
12 |
13 | def __try_redis(ret=None):
14 | def try_redis(func):
15 | @wraps(func)
16 | def try_func(*args, **kwargs):
17 | try:
18 | res = func(*args, **kwargs)
19 | except RedisError:
20 | cache.logger.error(f"Redis error with {args} {kwargs}", exc_info=True, stack_info=True)
21 | return ret
22 | return res
23 |
24 | return try_func
25 |
26 | return try_redis
27 |
28 |
29 | @__try_redis(None)
30 | def get_msg_from_cache(msg_id: int):
31 | msg = cache.hgetall(f"{CACHE_PREFIX}:msg:{msg_id}")
32 | if len(msg) != 4:
33 | return None
34 | return [msg.get("Email", ""),
35 | msg.get("Content"),
36 | datetime.fromtimestamp(float(msg.get("UpdateTime", 0.0))),
37 | msg.get("Secret", "False") == "True"]
38 |
39 |
40 | @__try_redis(None)
41 | def write_msg_to_cache(msg_id: int, email: str, content: str, update_time: str | datetime, secret: bool,
42 | is_db_bit=False):
43 | cache_name = f"{CACHE_PREFIX}:msg:{msg_id}"
44 | cache.delete(cache_name)
45 | cache.hset(cache_name, mapping={
46 | "Email": email,
47 | "Content": content,
48 | "UpdateTime": datetime.timestamp(update_time),
49 | "Secret": str(secret == DBBit.BIT_1 if is_db_bit else secret)
50 | })
51 | cache.expire(cache_name, CACHE_TIME)
52 |
53 |
54 | @__try_redis(None)
55 | def delete_msg_from_cache(msg_id: int):
56 | cache.delete(f"{CACHE_PREFIX}:msg:{msg_id}")
57 |
58 |
59 | @__try_redis(None)
60 | def get_msg_cout_from_cache():
61 | count = cache.get(f"{CACHE_PREFIX}:msg_count")
62 | if count is not None:
63 | return int(count)
64 |
65 |
66 | @__try_redis(None)
67 | def write_msg_count_to_cache(count):
68 | count = cache.set(f"{CACHE_PREFIX}:msg_count", str(count))
69 | cache.expire(f"{CACHE_PREFIX}:msg_count", CACHE_TIME)
70 | return count
71 |
72 |
73 | @__try_redis(None)
74 | def delete_msg_count_from_cache():
75 | cache.delete(f"{CACHE_PREFIX}:msg_count")
76 |
77 |
78 | @__try_redis(None)
79 | def get_user_msg_count_from_cache(user_id: int):
80 | count = cache.get(f"{CACHE_PREFIX}:msg_count:{user_id}")
81 | if count is not None:
82 | return int(count)
83 |
84 |
85 | @__try_redis(None)
86 | def write_user_msg_count_to_cache(user_id, count):
87 | cache_name = f"{CACHE_PREFIX}:msg_count:{user_id}"
88 | count = cache.set(cache_name, str(count))
89 | cache.expire(cache_name, CACHE_TIME)
90 | return count
91 |
92 |
93 | @__try_redis(None)
94 | def delete_user_msg_count_from_cache(user_id):
95 | cache.delete(f"{CACHE_PREFIX}:msg_count:{user_id}")
96 |
97 |
98 | @__try_redis(None)
99 | def delete_all_user_msg_count_from_cache():
100 | for i in cache.keys(f"{CACHE_PREFIX}:msg_count:*"):
101 | cache.delete(i)
102 |
103 |
104 | @__try_redis(None)
105 | def get_blog_from_cache(blog_id: int):
106 | blog = cache.hgetall(f"{CACHE_PREFIX}:blog:{blog_id}")
107 | if len(blog) != 7:
108 | return None
109 | return [int(blog.get("Auth", -1)),
110 | blog.get("Title"),
111 | blog.get("SubTitle"),
112 | blog.get("Content"),
113 | datetime.fromtimestamp(float(blog.get("UpdateTime", 0.0))),
114 | datetime.fromtimestamp(float(blog.get("CreateTime", 0.0))),
115 | blog.get("Top", "False") == "True"]
116 |
117 |
118 | @__try_redis(None)
119 | def write_blog_to_cache(blog_id: int, auth_id: str, title: str, subtitle: str, content: str,
120 | update_time: str | datetime, create_time: str | datetime, top: bool, is_db_bit=False):
121 | cache_name = f"{CACHE_PREFIX}:blog:{blog_id}"
122 | cache.delete(cache_name)
123 | cache.hset(cache_name, mapping={
124 | "Auth": auth_id,
125 | "Title": title,
126 | "SubTitle": subtitle,
127 | "Content": content,
128 | "UpdateTime": datetime.timestamp(update_time),
129 | "CreateTime": datetime.timestamp(create_time),
130 | "Top": str(top == DBBit.BIT_1 if is_db_bit else top)
131 | })
132 | cache.expire(cache_name, CACHE_TIME)
133 |
134 |
135 | @__try_redis(None)
136 | def delete_blog_from_cache(blog_id: int):
137 | cache.delete(f"{CACHE_PREFIX}:blog:{blog_id}")
138 |
139 |
140 | @__try_redis(None)
141 | def get_blog_count_from_cache():
142 | count = cache.get(f"{CACHE_PREFIX}:blog_count")
143 | if count is not None:
144 | return int(count)
145 |
146 |
147 | @__try_redis(None)
148 | def write_blog_count_to_cache(count):
149 | count = cache.set(f"{CACHE_PREFIX}:blog_count", str(count))
150 | cache.expire(f"{CACHE_PREFIX}:blog_count", CACHE_TIME)
151 | return count
152 |
153 |
154 | @__try_redis(None)
155 | def delete_blog_count_from_cache():
156 | cache.delete(f"{CACHE_PREFIX}:blog_count")
157 |
158 |
159 | @__try_redis(None)
160 | def get_archive_blog_count_from_cache(archive_id: int):
161 | count = cache.get(f"{CACHE_PREFIX}:blog_count:archive:{archive_id}")
162 | if count is not None:
163 | return int(count)
164 |
165 |
166 | @__try_redis(None)
167 | def write_archive_blog_count_to_cache(archive_id, count):
168 | cache_name = f"{CACHE_PREFIX}:blog_count:archive:{archive_id}"
169 | count = cache.set(cache_name, str(count))
170 | cache.expire(cache_name, CACHE_TIME)
171 | return count
172 |
173 |
174 | @__try_redis(None)
175 | def delete_all_archive_blog_count_from_cache():
176 | for i in cache.keys(f"{CACHE_PREFIX}:blog_count:archive:*"):
177 | cache.delete(i)
178 |
179 |
180 | @__try_redis(None)
181 | def delete_archive_blog_count_from_cache(archive_id: int):
182 | cache.delete(f"{CACHE_PREFIX}:blog_count:archive:{archive_id}")
183 |
184 |
185 | @__try_redis(None)
186 | def get_user_blog_count_from_cache(user_id: int):
187 | count = cache.get(f"{CACHE_PREFIX}:blog_count:user:{user_id}")
188 | if count is not None:
189 | return int(count)
190 |
191 |
192 | @__try_redis(None)
193 | def write_user_blog_count_to_cache(user_id, count):
194 | cache_name = f"{CACHE_PREFIX}:blog_count:user:{user_id}"
195 | count = cache.set(cache_name, str(count))
196 | cache.expire(cache_name, CACHE_TIME)
197 | return count
198 |
199 |
200 | @__try_redis(None)
201 | def delete_all_user_blog_count_from_cache():
202 | for i in cache.keys(f"{CACHE_PREFIX}:blog_count:user:*"):
203 | cache.delete(i)
204 |
205 |
206 | @__try_redis(None)
207 | def delete_user_blog_count_from_cache(user_id: int):
208 | cache.delete(f"{CACHE_PREFIX}:blog_count:user:{user_id}")
209 |
210 |
211 | @__try_redis(None)
212 | def get_archive_from_cache(archive_id: int):
213 | archive = cache.hgetall(f"{CACHE_PREFIX}:archive:{archive_id}")
214 | if len(archive) != 2:
215 | return None
216 | return [archive.get("Name", ""), archive.get("DescribeText")]
217 |
218 |
219 | @__try_redis(None)
220 | def write_archive_to_cache(archive_id: int, name: str, describe: str):
221 | cache_name = f"{CACHE_PREFIX}:archive:{archive_id}"
222 | cache.delete(cache_name)
223 | cache.hset(cache_name, mapping={
224 | "Name": name,
225 | "DescribeText": describe,
226 | })
227 | cache.expire(cache_name, CACHE_TIME)
228 |
229 |
230 | @__try_redis(None)
231 | def delete_archive_from_cache(archive_id: int):
232 | cache.delete(f"{CACHE_PREFIX}:archive:{archive_id}")
233 |
234 |
235 | @__try_redis(None)
236 | def get_blog_archive_from_cache(blog_id: int):
237 | blog_archive = cache.lrange(f"{CACHE_PREFIX}:blog_archive:{blog_id}", 0, -1)
238 | if len(blog_archive) == 0:
239 | return None
240 | elif blog_archive[0] == '-1':
241 | return []
242 | return blog_archive
243 |
244 |
245 | @__try_redis(None)
246 | def write_blog_archive_to_cache(blog_id: int, archive):
247 | cache_name = f"{CACHE_PREFIX}:blog_archive:{blog_id}"
248 | cache.delete(cache_name)
249 | if len(archive) == 0:
250 | cache.rpush(cache_name, -1)
251 | else:
252 | cache.rpush(cache_name, *archive)
253 | cache.expire(cache_name, CACHE_TIME)
254 |
255 |
256 | @__try_redis(None)
257 | def delete_blog_archive_from_cache(blog_id: int):
258 | cache.delete(f"{CACHE_PREFIX}:blog_archive:{blog_id}")
259 |
260 |
261 | @__try_redis(None)
262 | def delete_all_blog_archive_from_cache():
263 | for i in cache.keys(f"{CACHE_PREFIX}:blog_archive:*"):
264 | cache.delete(i)
265 |
266 |
267 | @__try_redis(None)
268 | def get_comment_from_cache(comment_id: int):
269 | comment = cache.hgetall(f"{CACHE_PREFIX}:comment:{comment_id}")
270 | if len(comment) != 4:
271 | return None
272 | return [comment.get("BlogID", ""),
273 | comment.get("Email", ""),
274 | comment.get("Content", ""),
275 | datetime.fromtimestamp(float(comment.get("UpdateTime", 0.0)))]
276 |
277 |
278 | @__try_redis(None)
279 | def write_comment_to_cache(comment_id: int, blog_id: str, email: str, content: str, update_time: str | datetime):
280 | cache_name = f"{CACHE_PREFIX}:comment:{comment_id}"
281 | cache.delete(cache_name)
282 | cache.hset(cache_name, mapping={
283 | "BlogID": blog_id,
284 | "Email": email,
285 | "Content": content,
286 | "UpdateTime": datetime.timestamp(update_time)
287 | })
288 | cache.expire(cache_name, CACHE_TIME)
289 |
290 |
291 | @__try_redis(None)
292 | def delete_comment_from_cache(comment_id: int):
293 | cache.delete(f"{CACHE_PREFIX}:comment:{comment_id}")
294 |
295 |
296 | @__try_redis(None)
297 | def get_user_comment_count_from_cache(user_id: int):
298 | count = cache.get(f"{CACHE_PREFIX}:comment_count:{user_id}")
299 | if count is not None:
300 | return int(count)
301 |
302 |
303 | @__try_redis(None)
304 | def write_user_comment_count_to_cache(user_id, count):
305 | cache_name = f"{CACHE_PREFIX}:comment_count:{user_id}"
306 | count = cache.set(cache_name, str(count))
307 | cache.expire(cache_name, CACHE_TIME)
308 | return count
309 |
310 |
311 | @__try_redis(None)
312 | def delete_user_comment_count_from_cache(user_id: int):
313 | cache.delete(f"{CACHE_PREFIX}:comment_count:{user_id}")
314 |
315 |
316 | @__try_redis(None)
317 | def delete_all_user_comment_count_from_cache():
318 | for i in cache.keys(f"{CACHE_PREFIX}:comment_count:*"):
319 | cache.delete(i)
320 |
321 |
322 | @__try_redis(None)
323 | def get_user_from_cache(email: str):
324 | user = cache.hgetall(f"{CACHE_PREFIX}:user:{email}")
325 | if len(user) != 3:
326 | return None
327 | return [user.get("PasswdHash", ""),
328 | int(user.get("Role", "")),
329 | int(user.get("ID", ""))]
330 |
331 |
332 | @__try_redis(None)
333 | def write_user_to_cache(email: str, passwd_hash: str, role: int, user_id: int):
334 | cache_name = f"{CACHE_PREFIX}:user:{email}"
335 | cache.delete(cache_name)
336 | cache.hset(cache_name, mapping={
337 | "PasswdHash": passwd_hash,
338 | "Role": role,
339 | "ID": user_id,
340 | })
341 | cache.expire(cache_name, CACHE_TIME)
342 |
343 |
344 | @__try_redis(None)
345 | def delete_user_from_cache(email: str):
346 | cache.delete(f"{CACHE_PREFIX}:user:{email}")
347 |
348 |
349 | @__try_redis(None)
350 | def get_user_email_from_cache(user_id: int):
351 | email = cache.get(f"{CACHE_PREFIX}:user_email:{user_id}")
352 | if email is None or len(email) == 0:
353 | return None
354 | return email
355 |
356 |
357 | @__try_redis(None)
358 | def write_user_email_to_cache(user_id: int, email: str):
359 | cache_name = f"{CACHE_PREFIX}:user_email:{user_id}"
360 | cache.set(cache_name, email)
361 | cache.expire(cache_name, CACHE_TIME)
362 |
363 |
364 | @__try_redis(None)
365 | def delete_user_email_from_cache(user_id: int):
366 | cache.delete(f"{CACHE_PREFIX}:user_email:{user_id}")
367 |
368 |
369 | @__try_redis(None)
370 | def get_role_name_from_cache(role_id: int):
371 | role_name = cache.get(f"{CACHE_PREFIX}:role_name:{role_id}")
372 | if role_name is None or len(role_name) == 0:
373 | return None
374 | return role_name
375 |
376 |
377 | @__try_redis(None)
378 | def write_role_name_to_cache(role_id: int, name: str):
379 | cache_name = f"{CACHE_PREFIX}:role_name:{role_id}"
380 | cache.set(cache_name, name)
381 | cache.expire(cache_name, CACHE_TIME)
382 |
383 |
384 | @__try_redis(None)
385 | def delete_role_name_from_cache(role_id: int):
386 | cache.delete(f"{CACHE_PREFIX}:role_name:{role_id}")
387 |
388 |
389 | def get_role_operate_from_cache(role_id: int, operate: str):
390 | res = cache.get(f"{CACHE_PREFIX}:operate:{role_id}:{operate}")
391 | if res is None or len(res) == 0:
392 | return None
393 | return res == "True"
394 |
395 |
396 | @__try_redis(None)
397 | def write_role_operate_to_cache(role_id: int, operate: str, res: bool):
398 | cache_name = f"{CACHE_PREFIX}:operate:{role_id}:{operate}"
399 | cache.set(cache_name, str(res))
400 | cache.expire(cache_name, CACHE_TIME)
401 |
402 |
403 | @__try_redis(None)
404 | def delete_role_operate_from_cache(role_id: int):
405 | for i in cache.keys(f"{CACHE_PREFIX}:operate:{role_id}:*"):
406 | cache.delete(i)
407 |
408 |
409 | @__try_redis(None)
410 | def restart_clear_cache():
411 | """
412 | 重启服务时必须要清理的缓存
413 | 包括Hblog-Cache和Flask-Cache
414 | """
415 |
416 | # 删除全部Flask缓存
417 | for i in cache.keys("flask_cache:*"):
418 | cache.delete(i)
419 |
--------------------------------------------------------------------------------
/app/docx.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, render_template, abort, redirect, url_for, flash, make_response, g, request
2 | from flask_wtf import FlaskForm
3 | from flask_login import login_required, current_user
4 | from wtforms import HiddenField, TextAreaField, StringField, SelectMultipleField, SubmitField, ValidationError
5 | from wtforms.validators import DataRequired, Length
6 | from typing import Optional
7 |
8 | import app
9 | from sql.base import DBBit
10 | from sql.statistics import add_blog_click, add_archive_click
11 | from object.blog import BlogArticle
12 | from object.comment import Comment
13 | from object.archive import Archive
14 | from configure import conf
15 |
16 | docx = Blueprint("docx", __name__)
17 |
18 |
19 | class EditorMD(FlaskForm):
20 | content = TextAreaField("博客内容", validators=[DataRequired(message="必须输入博客文章")])
21 |
22 |
23 | class WriteBlogForm(EditorMD):
24 | title = StringField("标题", description="博文主标题",
25 | validators=[
26 | DataRequired(message="必须填写标题"),
27 | Length(1, 20, message="标题长度1-20个字符")])
28 | subtitle = StringField("副标题", description="博文副标题",
29 | validators=[Length(-1, 20, message="副标题长度20个字符以内")])
30 | archive = SelectMultipleField("归档", coerce=int)
31 | submit = SubmitField("提交博客")
32 |
33 | def __init__(self, default: bool = False, **kwargs):
34 | super().__init__(**kwargs)
35 | if default:
36 | self.content.data = "# Blog Title\n## Blog subtitle\nHello, World"
37 | archive = Archive.get_archive_list()
38 | self.archive_res = []
39 | self.archive_choices = [(-1, "None")]
40 | for i in archive:
41 | self.archive_res.append(i.id)
42 | self.archive_choices.append((i.id, f"{i.name} ({i.count})"))
43 | self.archive.choices = self.archive_choices
44 |
45 | def validate_archive(self, field):
46 | if -1 in field.data:
47 | if len(field.data) != 1:
48 | raise ValidationError("归档指定错误(none归档不能和其他归档同时被指定)")
49 | else:
50 | for i in field.data:
51 | if i not in self.archive_res:
52 | raise ValidationError("错误的归档被指定")
53 |
54 |
55 | class UpdateBlogForm(EditorMD):
56 | blog_id = HiddenField("ID", validators=[DataRequired()])
57 | submit = SubmitField("更新博客")
58 |
59 | def __init__(self, blog: Optional[BlogArticle] = None, **kwargs):
60 | super().__init__(**kwargs)
61 | if blog is not None:
62 | self.blog_id.data = blog.id
63 | self.content.data = blog.content
64 |
65 |
66 | class UpdateBlogArchiveForm(FlaskForm):
67 | blog_id = HiddenField("ID", validators=[DataRequired()])
68 | archive = SelectMultipleField("归档", coerce=int)
69 | add = SubmitField("加入归档")
70 | sub = SubmitField("去除归档")
71 |
72 | def __init__(self, blog: Optional[BlogArticle] = None, **kwargs):
73 | super().__init__(**kwargs)
74 | archive = Archive.get_archive_list()
75 | self.archive_res = []
76 | self.archive_choices = []
77 | for i in archive:
78 | archive_id = i.id
79 | self.archive_res.append(archive_id)
80 | self.archive_choices.append((archive_id, f"{i.name} ({i.count})"))
81 | self.archive.choices = self.archive_choices
82 | if blog is not None:
83 | self.archive_data = []
84 | self.archive.data = self.archive_data
85 | for a in blog.archive:
86 | a: Archive
87 | self.archive_data.append(a)
88 | self.blog_id.data = blog.id
89 |
90 | def validate_archive(self, field):
91 | for i in field.data:
92 | if i not in self.archive_res:
93 | raise ValidationError("错误的归档被指定")
94 |
95 |
96 | class WriteCommentForm(FlaskForm):
97 | content = TextAreaField("", description="评论正文",
98 | validators=[DataRequired(message="请输入评论的内容"),
99 | Length(1, 100, message="请输入1-100个字的评论")])
100 | submit = SubmitField("评论")
101 |
102 |
103 | def __load_docx_page(page: int, form: WriteBlogForm):
104 | if page < 1:
105 | app.HBlogFlask.print_user_opt_fail_log(f"Load docx list with error page({page})")
106 | abort(404)
107 | return
108 |
109 | blog_list = BlogArticle.get_blog_list(limit=20, offset=(page - 1) * 20)
110 | max_page = app.HBlogFlask.get_max_page(BlogArticle.get_blog_count(), 20)
111 | page_list = app.HBlogFlask.get_page("docx.docx_page", page, max_page)
112 | app.HBlogFlask.print_load_page_log(f"docx list (page: {page})")
113 | return render_template("docx/docx.html",
114 | page=page,
115 | cache_str=f":{page}",
116 | blog_list=blog_list,
117 | page_list=page_list,
118 | form=form,
119 | show_delete=current_user.check_role("DeleteBlog"))
120 |
121 |
122 | @docx.route('/')
123 | def docx_page():
124 | page = request.args.get("page", 1, type=int)
125 | return __load_docx_page(page, WriteBlogForm(True))
126 |
127 |
128 | @docx.route('/archive')
129 | def archive_page():
130 | page = request.args.get("page", 1, type=int)
131 | archive = request.args.get("archive", 1, type=int)
132 | if page < 1:
133 | app.HBlogFlask.print_user_opt_fail_log(f"Load archive-docx list with error page({page}) archive: {archive}")
134 | abort(404)
135 | return
136 |
137 | blog_list = BlogArticle.get_blog_list(archive_id=archive, limit=20, offset=(page - 1) * 20)
138 | max_page = app.HBlogFlask.get_max_page(BlogArticle.get_blog_count(archive_id=archive), 20)
139 | page_list = app.HBlogFlask.get_page("docx.archive_page", page, max_page)
140 | add_archive_click(archive)
141 | app.HBlogFlask.print_load_page_log(f"archive-docx list (archive-id: {archive} page: {page})")
142 | return render_template("docx/docx.html",
143 | page=page,
144 | cache_str=f":{page}",
145 | blog_list=blog_list,
146 | page_list=page_list,
147 | form=None)
148 |
149 |
150 | def __load_article_page(blog_id: int, form: WriteCommentForm,
151 | view: Optional[UpdateBlogForm] = None,
152 | archive: Optional[UpdateBlogArchiveForm] = None):
153 | article = BlogArticle(blog_id)
154 | if article is None:
155 | app.HBlogFlask.print_user_opt_fail_log(f"Load article with error id({blog_id})")
156 | abort(404)
157 | return
158 | app.HBlogFlask.print_load_page_log(f"article (id: {blog_id})")
159 | if view is None:
160 | view = UpdateBlogForm(article)
161 | if archive is None:
162 | archive = UpdateBlogArchiveForm(article)
163 | add_blog_click(article.id)
164 | return render_template("docx/article.html",
165 | article=article,
166 | cache_str=f":{article.id}",
167 | archive_list=article.archive,
168 | form=form,
169 | view=view,
170 | archive=archive,
171 | can_update=current_user.check_role("WriteBlog"),
172 | show_delete=current_user.check_role("DeleteComment"),
173 | show_email=current_user.check_role("ReadUserInfo"))
174 |
175 |
176 | @docx.route('/article')
177 | def article_page():
178 | blog_id = request.args.get("blog", 1, type=int)
179 | return __load_article_page(blog_id, WriteCommentForm())
180 |
181 |
182 | @docx.route('/article/download')
183 | def article_down_page():
184 | blog_id = request.args.get("blog", 1, type=int)
185 | article = BlogArticle(blog_id)
186 | if article is None:
187 | app.HBlogFlask.print_user_opt_fail_log(f"Download article with error id({blog_id})")
188 | abort(404)
189 | return
190 |
191 | response = make_response(article.content)
192 | response.headers["Content-Disposition"] = f"attachment;filename={article.title.encode().decode('latin-1')}.md"
193 | app.HBlogFlask.print_load_page_log(f"download article (id: {blog_id})")
194 | return response
195 |
196 |
197 | @docx.route('/article/create', methods=["POST"])
198 | @login_required
199 | @app.form_required(WriteBlogForm,
200 | "write blog",
201 | lambda form: __load_docx_page(request.args.get("page", 1, type=int), form))
202 | @app.role_required("WriteBlog", "write blog")
203 | def create_docx_page():
204 | form: WriteBlogForm = g.form
205 | title = form.title.data
206 | subtitle = form.subtitle.data
207 | archive = []
208 | if -1 not in form.archive.data:
209 | for i in form.archive.data:
210 | i = Archive(i)
211 | if i is not None:
212 | archive.append(i)
213 |
214 | if BlogArticle.create(title, subtitle, form.content.data, archive, current_user):
215 | app.HBlogFlask.print_sys_opt_success_log("write blog")
216 | flash(f"博客 {title} 发表成功")
217 | else:
218 | app.HBlogFlask.print_sys_opt_fail_log("write blog")
219 | flash(f"博客 {title} 发表失败")
220 | return redirect(url_for("docx.docx_page", page=1))
221 |
222 |
223 | @docx.route('/article/update', methods=["POST"])
224 | @login_required
225 | @app.form_required(UpdateBlogForm, "update blog",
226 | lambda form: __load_article_page(form.id.data, WriteCommentForm(), form))
227 | @app.role_required("WriteBlog", "write blog")
228 | def update_docx_page():
229 | form: UpdateBlogForm = g.form
230 | if BlogArticle(form.blog_id.data).update(form.content.data):
231 | app.HBlogFlask.print_sys_opt_success_log("update blog")
232 | flash("博文更新成功")
233 | else:
234 | app.HBlogFlask.print_sys_opt_fail_log("update blog")
235 | flash("博文更新失败")
236 | return redirect(url_for("docx.article_page", blog=form.blog_id.data))
237 |
238 |
239 | @docx.route("/article/delete")
240 | @login_required
241 | @app.role_required("DeleteBlog", "delete blog")
242 | def delete_blog_page():
243 | blog_id = request.args.get("blog", None, type=int)
244 | if not blog_id:
245 | return abort(400)
246 | if BlogArticle(blog_id).delete():
247 | app.HBlogFlask.print_sys_opt_success_log("delete blog")
248 | flash("博文删除成功")
249 | else:
250 | app.HBlogFlask.print_sys_opt_fail_log("delete blog")
251 | flash("博文删除失败")
252 | return redirect(url_for("docx.docx_page", page=1))
253 |
254 |
255 | @docx.route("/article/set/top")
256 | @login_required
257 | @app.role_required("WriteBlog", "set blog top")
258 | def set_blog_top_page():
259 | blog_id = request.args.get("blog", None, type=int)
260 | top = request.args.get("top", 0, type=int) != 0
261 | if not blog_id:
262 | return abort(400)
263 | blog = BlogArticle(blog_id)
264 | blog.top = top
265 | if top == blog.top:
266 | app.HBlogFlask.print_sys_opt_success_log(f"set blog top ({top})")
267 | flash(f"博文{'取消' if not top else ''}置顶成功")
268 | else:
269 | app.HBlogFlask.print_sys_opt_fail_log(f"set blog top ({top})")
270 | flash(f"博文{'取消' if not top else ''}置顶失败")
271 | return redirect(url_for("docx.article_page", blog=blog_id))
272 |
273 |
274 | @docx.route("/article/set/archive", methods=["POST"])
275 | @login_required
276 | @app.form_required(UpdateBlogArchiveForm, "update archive",
277 | lambda form: __load_article_page(form.id.data, WriteCommentForm(), UpdateBlogForm(), form))
278 | @app.role_required("WriteBlog", "update archive")
279 | def update_archive_page():
280 | form: UpdateBlogArchiveForm = g.form
281 | article = BlogArticle(form.blog_id.data)
282 | add = request.args.get("add", 0, type=int) != 0
283 | for i in form.archive.data:
284 | if add:
285 | article.add_to_archive(i)
286 | else:
287 | article.sub_from_archive(i)
288 | flash("归档设定完成")
289 | return redirect(url_for("docx.article_page", blog=form.blog_id.data))
290 |
291 |
292 | @docx.route('/comment/create', methods=["POST"])
293 | @login_required
294 | @app.form_required(WriteCommentForm, "write comment",
295 | lambda form: __load_article_page(request.args.get("blog", 1, type=int), form))
296 | @app.role_required("WriteComment", "write comment")
297 | def comment_page():
298 | blog_id = request.args.get("blog", 1, type=int)
299 | form: WriteCommentForm = g.form
300 | content = form.content.data
301 | if Comment.create(BlogArticle(blog_id), current_user, content):
302 | app.HBlogFlask.print_user_opt_success_log("comment")
303 | flash("评论成功")
304 | else:
305 | app.HBlogFlask.print_user_opt_error_log("comment")
306 | flash("评论失败")
307 | return redirect(url_for("docx.article_page", blog=blog_id))
308 |
309 |
310 | @docx.route("/comment/delete")
311 | @login_required
312 | @app.role_required("DeleteComment", "delete comment")
313 | def delete_comment_page():
314 | comment_id = request.args.get("comment", 1, type=int)
315 | comment = Comment(comment_id)
316 | blog_id = comment.blog.id
317 | if blog_id == -1:
318 | abort(404)
319 |
320 | if comment.delete():
321 | app.HBlogFlask.print_sys_opt_success_log("delete comment")
322 | flash("博文评论删除成功")
323 | else:
324 | app.HBlogFlask.print_sys_opt_fail_log("delete comment")
325 | flash("博文评论删除失败")
326 | return redirect(url_for("docx.article_page", blog=blog_id))
327 |
328 |
329 | @docx.context_processor
330 | @app.cache.cached(timeout=conf["CACHE_EXPIRE"], key_prefix="inject_base:docx")
331 | def inject_base():
332 | """ docx 默认模板变量 """
333 | return {"top_nav": ["", "", "active", "", "", ""]}
334 |
--------------------------------------------------------------------------------