├── .env
├── .flaskenv
├── .gitignore
├── .gitpod.yml
├── Dockerfile
├── README.md
├── app.py
├── docker-compose.yml
├── docker-entrypoint.sh
├── requirements.txt
├── supervisor.conf
├── tests
├── __init__.py
└── test_admin_models.py
├── uwsgi.ini
├── wait_for_db.py
└── yublog
├── __init__.py
├── config.py
├── exceptions.py
├── extensions
├── __init__.py
└── picbed
│ ├── __init__.py
│ └── qiniu.py
├── forms.py
├── models.py
├── static
├── atom.xml
├── css
│ ├── admin.css
│ ├── blog.css
│ ├── column.css
│ ├── fontello.css
│ └── mobile.css
├── font
│ ├── fontello.eot
│ ├── fontello.svg
│ ├── fontello.ttf
│ ├── fontello.woff
│ └── fontello.woff2
├── images
│ ├── 404.jpg
│ ├── alipay.jpg
│ ├── baifeng.jpg
│ ├── favicon.ico
│ └── wechatpay.jpg
├── js
│ ├── admin.js
│ ├── comment.js
│ ├── image_util.js
│ ├── main.js
│ ├── myStorage.js
│ ├── myToc.js
│ ├── picbed.js
│ └── sticky.js
├── lib
│ ├── default.css
│ ├── fontello.min.css
│ ├── headroom.min.js
│ └── jquery.min.js
├── robots.txt
├── sitemap.xml
└── upload
│ ├── hello-world.md
│ ├── image
│ └── .gitignore
│ └── root.txt
├── templates
├── _admin_pagination.html
├── _comment.html
├── _pagination.html
├── admin
│ ├── change_password.html
│ ├── comment.html
│ ├── draft.html
│ ├── edit_link.html
│ ├── edit_page.html
│ ├── edit_post.html
│ ├── edit_sidebox.html
│ ├── edit_talk.html
│ ├── index.html
│ ├── link.html
│ ├── login.html
│ ├── page.html
│ ├── post.html
│ ├── profile.html
│ ├── sidebox.html
│ ├── talk.html
│ └── upload_file.html
├── admin_base.html
├── admin_column
│ ├── column.html
│ ├── columns.html
│ ├── edit_article.html
│ └── edit_column.html
├── admin_mail.html
├── base.html
├── column
│ ├── article.html
│ ├── column.html
│ ├── enter_password.html
│ └── index.html
├── column_base.html
├── error
│ ├── 404.html
│ └── 500.html
├── image
│ ├── index.html
│ └── path.html
├── main
│ ├── archives.html
│ ├── category.html
│ ├── friends.html
│ ├── index.html
│ ├── page.html
│ ├── post.html
│ ├── results.html
│ ├── tag.html
│ └── talk.html
├── plugin
│ └── picbed.html
└── user_mail.html
├── utils
├── __init__.py
├── as_sync.py
├── cache
│ ├── __init__.py
│ └── model.py
├── comment.py
├── commit.py
├── emails.py
├── functools.py
├── html.py
├── image.py
├── log.py
├── pxfilter.py
├── save.py
├── times.py
└── validators.py
└── views
├── __init__.py
├── admin.py
├── api.py
├── column.py
├── error.py
├── image.py
├── main.py
└── site.py
/.env:
--------------------------------------------------------------------------------
1 | # db configuration
2 | MYSQL_HOST="127.0.0.1"
3 | MYSQL_DATABASE="yublog"
4 | MYSQL_PASSWORD="password"
5 | # admin configuration
6 | ADMIN_LOGIN_NAME="yublog"
7 | ADMIN_PASSWORD="password"
8 | # email sender configuration
9 | MAIL_USERNAME=""
10 | MAIL_PASSWORD=""
11 | # email receiver configuration
12 | ADMIN_MAIL=""
13 | # qiniu secret configuration
14 | QN_ACCESS_KEY=""
15 | QN_SECRET_KEY=""
16 | # redis configuration
17 | CACHE_REDIS_HOST="127.0.0.1"
18 | CHCHE_REDIS_PASSWORD=""
19 |
--------------------------------------------------------------------------------
/.flaskenv:
--------------------------------------------------------------------------------
1 | FLASK_APP=yublog
2 | FLASK_ENV=development
3 | FLASK_DEBUG=0
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python:
2 | .env
3 | .idea
4 | .flaskenv
5 | .qiniu_pythonsdk_hostscache.json
6 | dist
7 | build
8 | __pycache__
9 | whooshee
10 | migrations
11 | venv
12 | .vscode/
13 | */static/upload/image/*
14 |
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 | tasks:
2 | - init: python3 -m pip install --upgrade pip && pip3 install -r ./requirements.txt
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.8.7
2 | LABEL creator="yokon" email="15152347277@163.com"
3 | ENV CONFIG=docker
4 |
5 | RUN mkdir /myapp
6 | WORKDIR /myapp
7 |
8 | COPY ./requirements.txt /myapp
9 | COPY ./docker-entrypoint.sh /myapp
10 | RUN pip install --upgrade pip \
11 | && pip install -i https://pypi.douban.com/simple/ -r requirements.txt \
12 | && pip install -i https://pypi.douban.com/simple/ uwsgi \
13 | && chmod +x docker-entrypoint.sh
14 |
15 | EXPOSE 9001
16 |
17 | # ENTRYPOINT ["bash", "docker-entrypoint.sh"]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # YuBlog
2 |
3 | ## 介绍
4 |
5 | [使用文档](https://www.yukunweb.com/page/YuBlog-document/)
6 |
7 | 功能强大的个人博客,功能齐全的管理后台,简洁大气的前端页面。
8 | 支持`markdown`文章编辑,代码高亮以及优雅美观的评论栏。
9 |
10 | ...
11 |
12 | ## 安装
13 |
14 | ```bash
15 | $ git clone git@github.com:yokonsan/yublog.git # 下载项目
16 | $ cd yublog
17 | $ pip install -r requirements.txt # 安装依赖
18 | ```
19 |
20 | ## 环境准备
21 |
22 | 安装`mysql`数据库,如需启用`redis`缓存,需安装`redis`。
23 |
24 | 缓存选项:
25 | - simple: 使用本地`Python`字典缓存,非线程安全。
26 | - redis: 使用`Redis`作为后端存储缓存值
27 | - filesystem: 使用文件系统来存储缓存值
28 |
29 | 配置文件 [yublog/config.py](yublog/config.py)
30 |
31 | 私密环境变量配置文件 [.env](.env)
32 |
33 |
34 | ## 启动
35 |
36 | ```bash
37 | $ flask init-db # 初始化数据库
38 | $ flask deploy # 生成默认数据
39 | $ flask run # 启动
40 | ```
41 |
42 | 默认地址:[127.0.0.1:5000](http://127.0.0.1:5000)
43 |
44 | 管理后台地址:[127.0.0.1:5000/admin](http://127.0.0.1:5000/admin)
45 |
46 | 账户密码:
47 | ```
48 | 账户:如未配置,默认 yublog
49 | 密码:如未配置,默认 password
50 | ```
51 |
52 | ## Docker
53 |
54 | ```bash
55 | $ docker-compose up -d
56 | ```
57 |
58 | 默认地址:[127.0.0.1:9001](http://127.0.0.1:9001)
59 |
60 | 管理后台地址:[127.0.0.1:9001/admin](http://127.0.0.1:9001/admin)
61 |
62 | 账户密码:
63 | ```
64 | 账户:如未配置,默认 yublog
65 | 密码:如未配置,默认 password
66 | ```
67 |
68 | **停止运行:**
69 |
70 | ```bash
71 | $ docker-compose down
72 | ```
73 |
74 | **查看日志:**
75 |
76 | ```bash
77 | $ docker-compose logs # 查看总的容器日志
78 | $ docker-compose logs yublog_web # 查看web应用运行日志
79 | ```
80 |
81 | ## 安装示例
82 |
83 | [yublog-installation-example](https://github.com/yokonsan/yublog-installation-example)
84 |
85 | ## Enjoy it.
86 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | from dotenv import load_dotenv
5 | from loguru import logger
6 |
7 | dotenv_path = os.path.join(os.path.dirname(__file__), ".env")
8 | if os.path.exists(dotenv_path):
9 | load_dotenv(dotenv_path)
10 |
11 | # logger config
12 | _lvl = os.getenv("LOG_LEVEL", default="TRACE")
13 | _format = "{time:%Y-%m-%d %H:%M:%S}> | " + \
14 | "{level}> | " + \
15 | "{process.id}-{thread.id} | " + \
16 | "{file.path}:{line}:{function}> " + \
17 | "- {message}>"
18 | logger.remove()
19 | logger.add(
20 | sys.stdout, level=_lvl, format=_format, colorize=True,
21 | )
22 |
23 | logger.add(
24 | f"log/open.log",
25 | level=_lvl,
26 | format=_format,
27 | rotation="00:00",
28 | retention="10 days",
29 | backtrace=True,
30 | diagnose=True,
31 | enqueue=True
32 | )
33 |
34 | from yublog import create_app # noqa
35 |
36 | app = create_app()
37 |
38 | if __name__ == '__main__':
39 | app.run()
40 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | web:
5 | build: .
6 | container_name: yublog_web
7 | networks:
8 | - frontend
9 | - backend
10 | volumes:
11 | - ./.:/myapp
12 | environment:
13 | - CONFIG=docker
14 | - MYSQL_HOST=db
15 | - CACHE_REDIS_HOST=cache
16 | - TZ=Asia/Shanghai
17 | - MYSQL_DATABASE=${MYSQL_DATABASE}
18 | - MYSQL_PASSWORD=${MYSQL_PASSWORD}
19 | # admin configuration
20 | - ADMIN_LOGIN_NAME=yinshi
21 | - ADMIN_PASSWORD=password
22 | # email sender configuration
23 | - MAIL_USERNAME=''
24 | - MAIL_PASSWORD=''
25 | # email receiver configuration
26 | - ADMIN_MAIL=''
27 | # qiniu secret configuration
28 | - QN_ACCESS_KEY=''
29 | - QN_SECRET_KEY=''
30 | # redis configuration
31 | - CACHE_REDIS_DB=0
32 | - CHCHE_REDIS_PASSWORD=''
33 | - LOG_LEVEL=ERROR
34 | ports:
35 | - "9001:9001"
36 | depends_on:
37 | - db
38 | - cache
39 | command: bash -c "bash docker-entrypoint.sh db 3306 root ${MYSQL_PASSWORD} ${MYSQL_DATABASE}"
40 |
41 | db:
42 | image: mysql:5.7
43 | container_name: yublog_db
44 | restart: always
45 | volumes:
46 | - ~/docker/mysql/data:/var/lib/mysql
47 | - ~/docker/mysql/conf:/etc/mysql/conf.d
48 | - ~/docker/mysql/logs:/logs
49 | networks:
50 | - backend
51 | environment:
52 | - TZ=Asia/Shanghai
53 | - MYSQL_DATABASE=${MYSQL_DATABASE}
54 | - MYSQL_ROOT_PASSWORD=${MYSQL_PASSWORD}
55 | command:
56 | --default-authentication-plugin=mysql_native_password
57 | --character-set-server=utf8mb4
58 | --collation-server=utf8mb4_general_ci
59 |
60 | cache:
61 | image: redis:latest
62 | container_name: yublog_cache
63 | networks:
64 | - backend
65 | environment:
66 | - TZ=Asia/Shanghai
67 | restart: always
68 | ports:
69 | - '6379:6379'
70 |
71 | networks:
72 | frontend:
73 | backend:
74 |
75 |
--------------------------------------------------------------------------------
/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | CODE=1
4 | while [[ CODE -ne 0 ]]
5 | do
6 | sleep 2
7 | echo "wait for db..."
8 | python wait_for_db.py $1 $2 $3 $4 $5
9 | CODE=$?
10 | done
11 |
12 | echo "flask init db"
13 | flask init-db
14 |
15 | echo "flask deploy"
16 | flask deploy
17 |
18 | echo "uwsgi start"
19 | uwsgi uwsgi.ini
20 | #uwsgi --socket 0.0.0.0:9001 --protocol=http -p 4 -t 8 -w -close-on-exec=true app:app
21 | #flask run --host 0.0.0.0
22 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | bleach==3.3.0
2 | flask-whooshee==0.4.1
3 | qiniu==7.2.0
4 | redis==4.5.4
5 | Flask==1.0.2
6 | Flask-Caching>=1.4
7 | Flask-Login==0.4.0
8 | Flask-Migrate==2.0.3
9 | Flask-Script==2.0.5
10 | SQLAlchemy==1.3.15
11 | Flask-SQLAlchemy>=2.5
12 | Flask-WTF==0.14.2
13 | html5lib==0.999999999
14 | itsdangerous==0.24
15 | Jinja2>=2.10.1
16 | Markdown==2.6.8
17 | MarkupSafe==0.23
18 | Pygments>=2.5.1
19 | PyMySQL==0.8
20 | Werkzeug==2.2.3
21 | WTForms==2.1
22 | python-dotenv~=0.17.0
23 | click~=7.1.2
24 | loguru==0.6.0
25 |
--------------------------------------------------------------------------------
/supervisor.conf:
--------------------------------------------------------------------------------
1 | [program:yublog]
2 | # 启动命令入口
3 | command=/project/yublog/venv/bin/uwsgi /project/yublog/config.ini
4 |
5 | # 命令程序所在目录
6 | directory=/project/yublog
7 | # 运行命令的用户名
8 | user=root
9 |
10 | autostart=true
11 | autorestart=true
12 | # 日志地址
13 | stdout_logfile=/project/yublog/logs/uwsgi_supervisor.log
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yokonsan/yublog/5c61703c283349cb30f2432cc3fb8f141be6800c/tests/__init__.py
--------------------------------------------------------------------------------
/uwsgi.ini:
--------------------------------------------------------------------------------
1 | [uwsgi]
2 | master = true
3 | socket = 0.0.0.0:9001
4 | protocol=http
5 |
6 | chdir = /myapp
7 |
8 | wsgi-file = app.py
9 |
10 | callable = app
11 |
12 | enable-threads = true
13 | processes = 4
14 | threads = 8
15 | buffer-size = 32768
--------------------------------------------------------------------------------
/wait_for_db.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import pymysql
3 |
4 |
5 | def wait_for(config):
6 | """wait for mysql init"""
7 | code = 0
8 | try:
9 | db = pymysql.connect(**config)
10 | db.close()
11 | except pymysql.err.InternalError:
12 | print("InternalError")
13 | code = 1
14 | except pymysql.err.OperationalError:
15 | print("OperationalError")
16 | code = 1
17 | finally:
18 | sys.exit(code)
19 |
20 |
21 | if __name__ == '__main__':
22 | wait_for({
23 | "host": sys.argv[1],
24 | "port": int(sys.argv[2]),
25 | "user": sys.argv[3],
26 | "password": sys.argv[4],
27 | "database": sys.argv[5]
28 | })
29 |
--------------------------------------------------------------------------------
/yublog/config.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from os import getenv
3 |
4 |
5 | class Config(object):
6 | CSRF_ENABLED = True
7 | SECRET_KEY = "yublog-guess"
8 | SQLALCHEMY_TRACK_MODIFICATIONS = False
9 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True
10 |
11 | POSTS_PER_PAGE = 10
12 | ADMIN_POSTS_PER_PAGE = 20
13 | ARCHIVES_POSTS_PER_PAGE = 20
14 | SEARCH_POSTS_PER_PAGE = 15
15 | COMMENTS_PER_PAGE = 10
16 | ADMIN_COMMENTS_PER_PAGE = 50
17 |
18 | UPLOAD_PATH = "./yublog/static/upload/"
19 | # 图片路径
20 | IMAGE_UPLOAD_PATH = "./yublog/static/upload/image/"
21 |
22 | # 数据库配置
23 | MYSQL_HOST = getenv("MYSQL_HOST") or "127.0.0.1"
24 | MYSQL_DATABASE = getenv("MYSQL_DATABASE") or "yublog"
25 | MYSQL_PASSWORD = getenv("MYSQL_PASSWORD") or "password"
26 | SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://root:{MYSQL_PASSWORD}@{MYSQL_HOST}:3306/{MYSQL_DATABASE}"
27 |
28 | # 博客信息
29 | # 管理员姓名
30 | ADMIN_NAME = "yublog"
31 | # 管理员登录信息
32 | ADMIN_LOGIN_NAME = "yublog"
33 | # 登录密码
34 | ADMIN_PASSWORD = getenv("ADMIN_PASSWORD") or "password"
35 | # 博客名
36 | SITE_NAME = "yublog"
37 | # 博客标题
38 | SITE_TITLE = "银时的博客"
39 | # 管理员简介
40 | ADMIN_PROFILE = "克制力,执行力"
41 |
42 | # RSS站点信息
43 | # 站点协议
44 | WEB_PROTOCOL = "http"
45 | # 站点域名
46 | WEB_URL = "www.domain.com"
47 | # 站点创建时间
48 | WEB_START_TIME = "2017-05-25"
49 | # 显示条数
50 | RSS_COUNTS = 10
51 |
52 | # 发送邮件用户登录
53 | MAIL_USERNAME = getenv("MAIL_USERNAME")
54 | # 客户端登录密码非正常登录密码
55 | MAIL_PASSWORD = getenv("MAIL_PASSWORD")
56 | MAIL_SERVER = getenv("MAIL_SERVER") or "smtp.qq.com"
57 | MAIL_PORT = getenv("MAIL_PORT") or "465"
58 |
59 | ADMIN_MAIL_SUBJECT_PREFIX = "yublog"
60 | ADMIN_MAIL_SENDER = "admin email"
61 | # 接收邮件通知的邮箱
62 | ADMIN_MAIL = getenv("ADMIN_MAIL")
63 | # 搜索最小字节
64 | WHOOSHEE_MIN_STRING_LEN = 1
65 |
66 | # cache 使用 Redis 数据库缓存配置
67 | CACHE_TYPE = "redis"
68 | CACHE_KEY_PREFIX = "yublog"
69 | CACHE_REDIS_HOST = getenv("CACHE_REDIS_HOST") or "127.0.0.1"
70 | CACHE_REDIS_PORT = 6379
71 | CACHE_REDIS_DB = 0
72 | CHCHE_REDIS_PASSWORD = getenv("CHCHE_REDIS_PASSWORD") or ""
73 |
74 | # 七牛云存储配置
75 | NEED_PIC_BED = False
76 | QN_ACCESS_KEY = getenv("QN_ACCESS_KEY") or ""
77 | QN_SECRET_KEY = getenv("QN_SECRET_KEY") or ""
78 | # 七牛空间名
79 | QN_PIC_BUCKET = "bucket-name"
80 | # 七牛外链域名
81 | QN_PIC_DOMAIN = "domain-url"
82 |
83 | @staticmethod
84 | def init_app(app):
85 | pass
86 |
87 |
88 | class DevelopmentConfig(Config):
89 | DEBUG = True
90 |
91 | @classmethod
92 | def init_app(cls, app):
93 | from loguru import logger
94 |
95 | class InterceptHandler(logging.Handler):
96 | def emit(self, record):
97 | logger_opt = logger.opt(depth=6, exception=record.exc_info)
98 | logger_opt.log(record.levelno, record.getMessage())
99 |
100 | Config.init_app(app)
101 | app.logger.addHandler(InterceptHandler())
102 | logging.basicConfig(handlers=[InterceptHandler()], level="INFO")
103 |
104 |
105 | class TestingConfig(Config):
106 | TESTING = True
107 |
108 |
109 | class ProductionConfig(Config):
110 | DEBUG = False
111 |
112 | @classmethod
113 | def init_app(cls, app):
114 | Config.init_app(app)
115 | # 把错误发给管理
116 | from logging.handlers import SMTPHandler
117 | credentials = None
118 | secure = None
119 | if getattr(cls, "MAIL_USERNAME", None) is not None:
120 | credentials = (cls.MAIL_USERNAME, cls.MAIL_PASSWORD)
121 | if getattr(cls, "MAIL_USE_TLS", None):
122 | secure = ()
123 | mail_handler = SMTPHandler(
124 | mailhost=(cls.MAIL_SERVER, cls.MAIL_PORT),
125 | fromaddr=cls.ADMIN_MAIL_SENDER,
126 | toaddrs=[cls.ADMIN_MAIL],
127 | subject=cls.ADMIN_MAIL_SUBJECT_PREFIX + " Application Error",
128 | credentials=credentials,
129 | secure=secure
130 | )
131 | mail_handler.setLevel(logging.ERROR)
132 | app.logger.addHandler(mail_handler)
133 |
134 |
135 | class DockerConfig(ProductionConfig):
136 | DEBUG = False
137 |
138 |
139 | config = {
140 | "development": DevelopmentConfig,
141 | "testing": TestingConfig,
142 | "production": ProductionConfig,
143 | "docker": DockerConfig,
144 | "default": DevelopmentConfig
145 | }
146 |
--------------------------------------------------------------------------------
/yublog/exceptions.py:
--------------------------------------------------------------------------------
1 | class NoCacheTypeException(Exception):
2 | """无此缓存类型异常"""
3 |
4 |
5 | class DuplicateEntryException(Exception):
6 | """唯一性字段重复异常"""
7 |
8 |
9 | class AppInitException(Exception):
10 | """应用初始化异常"""
11 |
--------------------------------------------------------------------------------
/yublog/extensions/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_sqlalchemy import SQLAlchemy
2 | from flask_login import LoginManager
3 | from flask_whooshee import Whooshee
4 | from flask_caching import Cache
5 | from flask_migrate import Migrate
6 |
7 | from yublog.extensions.picbed.qiniu import QiniuUpload
8 |
9 | migrate = Migrate()
10 | db = SQLAlchemy()
11 | whooshee = Whooshee()
12 | cache = Cache()
13 | qn = QiniuUpload()
14 | lm = LoginManager()
15 | lm.login_view = "admin.login"
16 |
--------------------------------------------------------------------------------
/yublog/extensions/picbed/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yokonsan/yublog/5c61703c283349cb30f2432cc3fb8f141be6800c/yublog/extensions/picbed/__init__.py
--------------------------------------------------------------------------------
/yublog/extensions/picbed/qiniu.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | from datetime import datetime
4 |
5 | import qiniu
6 |
7 |
8 | class QiniuUpload(object):
9 | """
10 | 集成七牛云存储操作
11 | """
12 | def __init__(self, app=None):
13 | self.app = app
14 | self.img_suffix = ['jpg', 'jpeg', 'png', 'gif']
15 | if app: self.init_app(app)
16 |
17 | def init_qiniu(self):
18 | self.qiniuer = qiniu.Auth(
19 | self.app.config.get('QN_ACCESS_KEY', ''),
20 | self.app.config.get('QN_SECRET_KEY', '')
21 | )
22 | self.bucket_manager = qiniu.BucketManager(self.qiniuer)
23 | self.bucket = self.app.config.get('QN_PIC_BUCKET', '')
24 | self.domain = self.app.config.get('QN_PIC_DOMAIN', '')
25 |
26 | def init_app(self, app):
27 | """
28 | 从应用程序设置初始化设置。
29 | :param app: Flask app
30 | """
31 | self.app = app
32 | self.init_qiniu()
33 |
34 | @staticmethod
35 | def _get_publish_time(timestamp):
36 | if timestamp:
37 | t = float(timestamp/10000000)
38 | return datetime.fromtimestamp(t).strftime('%Y-%m-%d %H:%M')
39 | return None
40 |
41 | @staticmethod
42 | def _get_file_size(size):
43 | if size:
44 | return float('%.2f' % (size / 1024))
45 | return 0
46 |
47 | def get_token(self):
48 | return self.qiniuer.upload_token(self.bucket)
49 |
50 | def parse_img_name(self, file, filename=None):
51 | """
52 | 解析出合法文件名
53 | :param file: 文件路径
54 | :param filename: 文件名
55 | :return: 文件名
56 | """
57 | suffix = os.path.splitext(os.path.basename(file))[1]
58 | if suffix not in self.img_suffix: return False
59 | if filename:
60 | filename = re.sub(r'[\/\\\:\*\?"<>|]', r'_', filename)
61 | if filename.find('.') == -1:
62 | filename += suffix
63 | else:
64 | filename = os.path.basename(file)
65 | return filename
66 |
67 | def upload_qn(self, filename, data):
68 | """
69 | :param filename: 文件所在路径,如有data则为图片名
70 | :param data: 图片二进制流
71 |
72 | :return: True or False
73 | """
74 | filename, data = filename, data
75 | token = self.get_token()
76 | key = filename
77 | try:
78 | ret, info = qiniu.put_data(token, key, data)
79 | return True if info.status_code == 200 else False
80 | except:
81 | return False
82 |
83 | def get_img_link(self, filename):
84 | return self.domain + '/' + filename
85 |
86 | def del_file(self, key):
87 | ret, info = self.bucket_manager.delete(self.bucket, key)
88 |
89 | return True if ret == {} else False
90 |
91 | def rename_file(self, key, key_to):
92 | ret, info = self.bucket_manager.rename(self.bucket, key, key_to)
93 |
94 | return True if ret == {} else False
95 |
96 | def upload_status(self, key):
97 | ret, info = self.bucket_manager.stat(self.bucket, key)
98 |
99 | return True if info.status_code == 200 else False
100 |
101 | def get_all_images(self):
102 | """
103 | :return: [{'name':'图片名', 'url': '图片url'}, {}]
104 | """
105 | images = []
106 | prefix = None
107 | limit = None
108 | delimiter = None # 列举出除'/'的所有文件以及以'/'为分隔的所有前缀
109 | marker = None # 标记
110 | ret, eof, info = self.bucket_manager.list(self.bucket, prefix, marker, limit, delimiter)
111 |
112 | for i in ret.get('items', []):
113 | if i.get('mimeType', '').startswith('image'):
114 | images.append({
115 | 'name': i.get('key', ''),
116 | 'url': self.domain + i.get('key', ''),
117 | 'time': self._get_publish_time(i.get('putTime', '')),
118 | 'size': self._get_file_size(i.get('fsize', 0))
119 | })
120 | return images
121 |
--------------------------------------------------------------------------------
/yublog/forms.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import (
3 | StringField,
4 | PasswordField,
5 | SubmitField,
6 | BooleanField,
7 | TextAreaField,
8 | Field,
9 | )
10 | from wtforms.validators import DataRequired
11 |
12 | from yublog.utils.times import nowstr
13 |
14 |
15 | class FormMixin:
16 | """mixin some method"""
17 |
18 | @property
19 | def filter_fields(self):
20 | return {
21 | "csrf_token",
22 | "submit",
23 | "save_draft",
24 | }
25 |
26 | @property
27 | def ins_fields(self):
28 | return set()
29 |
30 | def __fields(self):
31 | try:
32 | fields = getattr(self, "data").keys()
33 | except AttributeError:
34 | fields = (k for k, v in self.__dict__.items() if isinstance(v, Field))
35 |
36 | return fields
37 |
38 | def to_form(self, item, **kwargs):
39 | for field in filter(
40 | lambda x: x not in (self.filter_fields | self.ins_fields),
41 | self.__fields()
42 | ):
43 | f = getattr(self, field)
44 |
45 | if field in kwargs:
46 | f.data = kwargs[field]
47 | else:
48 | f.data = getattr(item, field)
49 |
50 | def to_model(self, item, **kwargs):
51 | for field in filter(
52 | lambda x: x not in (self.filter_fields | self.ins_fields),
53 | self.__fields()
54 | ):
55 | if field in kwargs:
56 | setattr(item, field, kwargs[field])
57 | else:
58 | form = getattr(self, field)
59 | setattr(item, field, form.data)
60 |
61 | def new_model(self, model, **kwargs):
62 | kws = {}
63 | for field in filter(
64 | lambda x: x not in (self.filter_fields | self.ins_fields),
65 | self.__fields()
66 | ):
67 | kws[field] = getattr(self, field).data
68 |
69 | if kwargs:
70 | kws.update(kwargs)
71 | return model(**kws)
72 |
73 |
74 | class AdminLogin(FlaskForm, FormMixin):
75 | username = StringField("username", validators=[DataRequired()])
76 | password = PasswordField("password", validators=[DataRequired()])
77 | remember_me = BooleanField("remember", default=False)
78 |
79 |
80 | class AdminWrite(FlaskForm, FormMixin):
81 | title = StringField("title", validators=[DataRequired()])
82 | create_time = StringField("datetime", validators=[DataRequired()], default=nowstr(fmt="%Y-%m-%d"))
83 | tags = StringField("tag", validators=[DataRequired()])
84 | category = StringField("category", validators=[DataRequired()])
85 | url_name = StringField("urlName", validators=[DataRequired()])
86 | body = TextAreaField("body", validators=[DataRequired()])
87 |
88 | save_draft = SubmitField("save")
89 | submit = SubmitField("submit")
90 |
91 |
92 | class AddPageForm(FlaskForm, FormMixin):
93 | title = StringField("title", validators=[DataRequired()])
94 | url_name = StringField("url_name", validators=[DataRequired()])
95 | body = TextAreaField("body", validators=[DataRequired()])
96 | enable_comment = BooleanField("can_comment")
97 | show_nav = BooleanField("is_nav")
98 | submit = SubmitField("submit")
99 |
100 |
101 | class SocialLinkForm(FlaskForm, FormMixin):
102 | link = StringField("url", validators=[DataRequired()])
103 | name = StringField("name", validators=[DataRequired()])
104 | submit = SubmitField("submit")
105 |
106 |
107 | class FriendLinkForm(FlaskForm, FormMixin):
108 | link = StringField("url", validators=[DataRequired()])
109 | name = StringField("name", validators=[DataRequired()])
110 | info = StringField("info", validators=[DataRequired()])
111 | submit2 = SubmitField("submit2")
112 |
113 | @property
114 | def ins_fields(self):
115 | return {"submit2"}
116 |
117 |
118 | class AdminSiteForm(FlaskForm, FormMixin):
119 | site_name = StringField("name", validators=[DataRequired()])
120 | site_title = StringField("title", validators=[DataRequired()])
121 |
122 | username = StringField("username", validators=[DataRequired()])
123 | profile = StringField("profile", validators=[DataRequired()])
124 |
125 | record_info = StringField("record info")
126 |
127 |
128 | class TalkForm(FlaskForm):
129 | talk = TextAreaField("talk", validators=[DataRequired()])
130 |
131 |
132 | class ColumnForm(FlaskForm, FormMixin):
133 | title = StringField("column", validators=[DataRequired()])
134 | create_time = StringField("datetime", validators=[DataRequired()], default=nowstr(fmt="%Y-%m-%d"))
135 | url_name = StringField("urlName", validators=[DataRequired()])
136 | password = StringField("password")
137 | body = TextAreaField("body", validators=[DataRequired()])
138 | submit = SubmitField("submit")
139 |
140 | @property
141 | def ins_fields(self):
142 | return {"password"}
143 |
144 |
145 | class ColumnArticleForm(FlaskForm, FormMixin):
146 | title = StringField("title", validators=[DataRequired()])
147 | create_time = StringField("datetime", validators=[DataRequired()], default=nowstr(fmt="%Y-%m-%d"))
148 | body = TextAreaField("body", validators=[DataRequired()])
149 | secrecy = BooleanField("secrecy")
150 | submit = SubmitField("submit")
151 |
152 |
153 | class SideBoxForm(FlaskForm, FormMixin):
154 | title = StringField("title")
155 | body = TextAreaField("body", validators=[DataRequired()])
156 | is_advertising = BooleanField("is_advertising")
157 | submit = SubmitField("submit")
158 |
159 |
160 | class ChangePasswordForm(FlaskForm):
161 | old_password = PasswordField("Old password", validators=[DataRequired()])
162 | password = PasswordField("New password", validators=[DataRequired()])
163 | password2 = PasswordField("Confirm new password", validators=[DataRequired()])
164 |
165 |
166 | class SearchForm(FlaskForm):
167 | search = StringField("Search", validators=[DataRequired()])
168 |
169 |
170 | class MobileSearchForm(FlaskForm):
171 | search = StringField("Search", validators=[DataRequired()])
172 |
173 |
174 | class CommentForm(FlaskForm, FormMixin):
175 | nickname = StringField("nickname", validators=[DataRequired()])
176 | email = StringField("email", validators=[DataRequired()])
177 | website = StringField("website")
178 | comment = TextAreaField("comment", validators=[DataRequired()])
179 |
180 |
181 | class ArticlePasswordForm(FlaskForm):
182 | password = StringField("password", validators=[DataRequired()])
183 |
184 |
185 | class AddImagePathForm(FlaskForm):
186 | path_name = StringField("new path", validators=[DataRequired()])
187 |
--------------------------------------------------------------------------------
/yublog/static/atom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 意外
4 | 银时的博客
5 |
6 |
7 | tag:www.yukunweb.com,2017-05-25://1
8 | 2021-02-11T00:00:00Z
9 | YuBlog
10 |
11 |
12 |
13 | 胡说八道
14 |
15 | tag:www.yukunweb.com,2021://1.2
16 | 2021-02-12T00:00:00Z
17 | 2021-02-11T00:00:00Z
18 | 胡说八道
19 |
20 | 银时
21 | http://www.yukunweb.com
22 |
23 |
24 | categorycategorycategory
]]>
25 |
26 |
27 |
28 |
29 | 企查查请求头反爬破解
30 |
31 | tag:www.yukunweb.com,2021://1.3
32 | 2021-02-12T00:00:00Z
33 | 2021-02-11T00:00:00Z
34 | 企查查请求头反爬破解
35 |
36 | 银时
37 | http://www.yukunweb.com
38 |
39 |
40 | 企查查请求头反爬破解]]>
41 |
42 |
43 |
44 |
45 | QCC请求头反爬破解
46 |
47 | tag:www.yukunweb.com,2021://1.1
48 | 2021-02-11T00:00:00Z
49 | 2021-02-11T00:00:00Z
50 | QCC请求头反爬破解
51 |
52 | 银时
53 | http://www.yukunweb.com
54 |
55 |
56 | QCC请求头反爬破解]]>
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/yublog/static/css/column.css:
--------------------------------------------------------------------------------
1 | /*column styles*/
2 | .site-box {
3 | border-radius: 0;
4 | }
5 |
6 | .column-menu-btn {
7 | display: block;
8 | position: absolute;
9 | right: 20px;
10 | top: 50%;
11 | transform: translateY(-50%);
12 |
13 | }
14 |
15 | .column-menu-btn button {
16 | margin-top: 2px;
17 | padding: 9px 10px;
18 | background: transparent;
19 | border: none;
20 | cursor: pointer;
21 | outline: none;
22 | }
23 |
24 | .btn-bar {
25 | background: #999;
26 | display: block;
27 | width: 22px;
28 | height: 2px;
29 | margin-bottom: 4px;
30 | border-radius: 1px;
31 | }
32 |
33 | .column-menu-btn button:hover > .btn-bar {
34 | background: #666;
35 | }
36 |
37 | .column-main,.article-main {
38 | margin: 70px auto;
39 | width: 800px;
40 | }
41 |
42 | .column-item,.column-articles {
43 | padding: 20px;
44 | }
45 |
46 | .column-title,.article-title {
47 | padding: 20px 20px 0;
48 | }
49 |
50 | .column-title h1,
51 | .article-title h1 {
52 | text-align: center;
53 | font-size: 24px;
54 | font-weight: 600;
55 | }
56 |
57 | .article-title h1 {
58 | text-align: left;
59 | display: inline-block;
60 | }
61 |
62 | .column-meta,.article-meta {
63 | font-size: 12px;
64 | color: #999;
65 | padding: 0 20px;
66 | margin: 10px 0;
67 | }
68 |
69 | .column-meta {
70 | text-align: center;
71 | }
72 |
73 | .column-body,.article-body {
74 | padding: 20px;
75 | line-height: 1.8;
76 | font-size: 15px;
77 | border-bottom: 1px solid #eee;
78 | }
79 |
80 | .column-btn {
81 | padding: 30px 0;
82 | text-align: center;
83 | }
84 |
85 | .love-column,.read-column {
86 | margin: 0 20px;
87 | padding: 8px 30px;
88 | font-size: 16px;
89 | background: #e6876c;
90 | border: none;
91 | outline: none;
92 | cursor: pointer;
93 | color: #fff;
94 | font-weight: 600;
95 | border-radius: 3px;
96 | }
97 |
98 | .love-column:hover,.read-column:hover {
99 | background: #e6542b;
100 | }
101 |
102 | .column-menu {
103 | padding: 0 20px;
104 | font-size: 18px;
105 | font-weight: 600;
106 | }
107 |
108 | .column-menu:after {
109 | display: block;
110 | content: " ";
111 | margin-top: 10px;
112 | width: 50px;
113 | border-bottom: 2px solid rgb(230, 135, 108);
114 | }
115 |
116 | .column-articles {
117 | margin-bottom: 30px;
118 | }
119 |
120 | .article-item {
121 | padding: 5px;
122 | margin: 20px 40px;
123 | font-size: 16px;
124 | border-bottom: 1px solid #eee;
125 | }
126 |
127 | .article-num {
128 | color: #999;
129 | margin-right: 15px;
130 | }
131 |
132 | .article-item a {
133 | color: #111;
134 | }
135 |
136 | .article-item a:hover {
137 | color: #888;
138 | }
139 |
140 |
141 | /* article styles */
142 |
143 | .article-item {
144 | padding: 20px;
145 | }
146 |
147 |
148 | .article-desc {
149 | font-size: 14px;
150 | }
151 |
152 | .article-desc img {
153 | margin: 0 auto;
154 | display: block;
155 | max-width: 100%;
156 | -moz-box-sizing: border-box;
157 | box-sizing: border-box;
158 | border-radius: 6px;
159 | box-shadow: 0 0 10px #ccc;
160 | }
161 |
162 | .menu-list {
163 | padding: 20px 10px;
164 | max-width: 220px;
165 | box-shadow: 0 3px 10px rgba(0,0,0,.1);
166 | height: 100%;
167 | float: left;
168 | width: 260px;
169 | background: #fff;
170 | border-right: 2px solid #ccc;
171 | margin-top: 0;
172 | z-index: 9999;
173 | position: fixed;
174 | overflow-y: auto;
175 | top: 0;
176 | left: 0;
177 | bottom: 0;
178 | box-sizing: border-box;
179 | }
180 |
181 | .menu-title {
182 | padding: 15px 0 0;
183 | font-size: 16px;
184 | }
185 |
186 | .menu-list ul {
187 | margin: 0;
188 | padding: 5px;
189 | list-style: none;
190 | }
191 |
192 | .menu-list ul li {
193 | font-size: 14px;
194 | padding: 5px 0;
195 | color: #999;
196 | border-bottom: 1px solid #eee;
197 | }
198 |
199 | .menu-list a {
200 | color: #666;
201 | }
202 |
203 | .menu-title a {
204 | color: #111;
205 | }
206 |
207 | .menu-list a:hover,.menu-list a.current {
208 | color: #111;
209 | font-weight: 600;
210 | }
211 |
212 | .article-btn {
213 | border-bottom: 1px solid #eee;
214 | }
215 |
216 | .prev-btn,.next-btn {
217 | display: inline-block;
218 | width: 50%;
219 | text-align: center;
220 | padding: 20px 0;
221 | color: #999;
222 | font-size: 16px;
223 | font-weight: 600;
224 | }
225 |
226 | .prev-btn {
227 | float: left;
228 | }
229 | .next-btn {
230 | float: right;
231 | }
232 |
233 | .prev-btn:hover,.next-btn:hover {
234 | color: #df846c;
235 | }
236 |
237 |
238 | /*index*/
239 | .column-list {
240 | padding: 20px;
241 | }
242 |
243 | .column-box {
244 | padding: 10px;
245 | }
246 |
247 | .column {
248 | font-size: 18px;
249 | font-weight: 600;
250 | color: #111;
251 | }
252 |
253 | .column:hover {
254 | color: #888;
255 | }
256 |
257 | .column-profile {
258 | padding: 10px 0;
259 | line-height: 1.7;
260 | border-bottom: 1px solid #eee;
261 | }
262 |
263 | /* image */
264 | .image-body {
265 | border-radius: 3px;
266 | border: 1px solid #ccc;
267 | }
268 |
269 | .image-item {
270 | padding: 50px 50px;
271 | }
272 |
273 | .path-row {
274 | padding: 6px 10px;
275 | border-bottom: 1px solid #eee;
276 | font-size: 15px;
277 | }
278 |
279 | /* .path-row a {
280 |
281 | } */
282 |
283 | .path-row a:hover {
284 | color: #888;
285 | border-bottom: 1px solid;
286 | }
287 |
288 | @media (max-width: 992px) {
289 | .site-wrap {
290 | width: 960px;
291 | }
292 | }
293 | @media (max-width: 768px) {
294 | .site-wrap,.footer {
295 | width: 100%;
296 | }
297 | .column-main,.article-main {
298 | width: 98%;
299 | }
300 | .column-title,.article-title {
301 | padding: 20px 0 0;
302 | }
303 | .column-body,.article-body {
304 | padding: 0;
305 | }
306 | /*mobile comment*/
307 | #comments {
308 | font-size: 10px;
309 | color: #999;
310 | padding: 10px;
311 | padding-bottom: 20px;
312 | }
313 |
314 | .textarea-container {
315 | padding: 0;
316 | }
317 |
318 | .textarea-container div.input-div {
319 | width: 100%;
320 | }
321 |
322 | .textarea-container input {
323 | float: none;
324 | }
325 |
326 | ol.comment-list,ul.comment-children {
327 | padding: 0;
328 | }
329 |
330 | .comment-content {
331 | margin-left: 45px;
332 | }
333 |
334 | .comment-children .comment-content {
335 | margin-left: 35px;
336 | }
337 | }
338 |
--------------------------------------------------------------------------------
/yublog/static/css/fontello.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'fontello';
3 | src: url('../font/fontello.eot?75132758');
4 | src: url('../font/fontello.eot?75132758#iefix') format('embedded-opentype'),
5 | url('../font/fontello.woff2?75132758') format('woff2'),
6 | url('../font/fontello.woff?75132758') format('woff'),
7 | url('../font/fontello.ttf?75132758') format('truetype'),
8 | url('../font/fontello.svg?75132758#fontello') format('svg');
9 | font-weight: normal;
10 | font-style: normal;
11 | }
12 | /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
13 | /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
14 | /*
15 | @media screen and (-webkit-min-device-pixel-ratio:0) {
16 | @font-face {
17 | font-family: 'fontello';
18 | src: url('../font/fontello.svg?75132758#fontello') format('svg');
19 | }
20 | }
21 | */
22 |
23 | [class^="icon-"]:before, [class*=" icon-"]:before {
24 | font-family: "fontello";
25 | font-style: normal;
26 | font-weight: normal;
27 | speak: none;
28 |
29 | display: inline-block;
30 | text-decoration: inherit;
31 | width: 1em;
32 | margin-right: .2em;
33 | text-align: center;
34 | /* opacity: .8; */
35 |
36 | /* For safety - reset parent styles, that can break glyph codes*/
37 | font-variant: normal;
38 | text-transform: none;
39 |
40 | /* fix buttons height, for twitter bootstrap */
41 | line-height: 1em;
42 |
43 | /* Animation center compensation - margins should be symmetric */
44 | /* remove if not needed */
45 | margin-left: .2em;
46 |
47 | /* you can be more comfortable with increased icons size */
48 | /* font-size: 120%; */
49 |
50 | /* Font smoothing. That was taken from TWBS */
51 | -webkit-font-smoothing: antialiased;
52 | -moz-osx-font-smoothing: grayscale;
53 |
54 | /* Uncomment for 3D effect */
55 | /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
56 | }
57 |
58 | .icon-link:before { content: '\e800'; } /* '' */
59 | .icon-tags:before { content: '\e801'; } /* '' */
60 | .icon-music:before { content: '\e802'; } /* '' */
61 | .icon-heart:before { content: '\e803'; } /* '' */
62 | .icon-heart-empty:before { content: '\e804'; } /* '' */
63 | .icon-mail:before { content: '\e805'; } /* '' */
64 | .icon-search:before { content: '\e806'; } /* '' */
65 | .icon-star:before { content: '\e807'; } /* '' */
66 | .icon-star-empty:before { content: '\e808'; } /* '' */
67 | .icon-user:before { content: '\e809'; } /* '' */
68 | .icon-video:before { content: '\e80a'; } /* '' */
69 | .icon-picture:before { content: '\e80b'; } /* '' */
70 | .icon-eye:before { content: '\e80c'; } /* '' */
71 | .icon-bookmark:before { content: '\e80d'; } /* '' */
72 | .icon-comment:before { content: '\e80e'; } /* '' */
73 | .icon-thumbs-up:before { content: '\e80f'; } /* '' */
74 | .icon-trash-empty:before { content: '\e810'; } /* '' */
75 | .icon-calendar:before { content: '\e811'; } /* '' */
76 | .icon-book:before { content: '\e812'; } /* '' */
77 | .icon-home:before { content: '\e813'; } /* '' */
78 | .icon-globe:before { content: '\f018'; } /* '' */
79 | .icon-github-circled:before { content: '\f09b'; } /* '' */
80 | .icon-rss:before { content: '\f09e'; } /* '' */
81 | .icon-mail-alt:before { content: '\f0e0'; } /* '' */
82 | .icon-comment-empty:before { content: '\f0e5'; } /* '' */
83 | .icon-folder-empty:before { content: '\f114'; } /* '' */
84 | .icon-doc-text-inv:before { content: '\f15c'; } /* '' */
85 | .icon-thumbs-up-alt:before { content: '\f164'; } /* '' */
86 | .icon-box:before { content: '\f187'; } /* '' */
87 | .icon-weibo:before { content: '\f18a'; } /* '' */
88 | .icon-share:before { content: '\f1e0'; } /* '' */
89 | .icon-trash:before { content: '\f1f8'; } /* '' */
--------------------------------------------------------------------------------
/yublog/static/css/mobile.css:
--------------------------------------------------------------------------------
1 | /*width < 960px*/
2 | .site-wrap {
3 | width: 100%;
4 | }
5 |
6 | .site-nav,.site-logo {
7 | display: none;
8 | }
9 |
10 | .sidebar {
11 | display: none;
12 | }
13 |
14 | .main {
15 | float: left;
16 | }
17 |
18 | .site-header {
19 | position: fixed;
20 | padding: 10px 0;
21 | color: #666;
22 | background: #fff;
23 | height: 35px;
24 | box-shadow: 0 0 3px rgba(98, 97, 97, 0.5);
25 | }
26 |
27 | .mobile-site-logo {
28 | text-align: left;
29 | display: block;
30 | padding-left: 20px;
31 | }
32 |
33 | .mobile-site-logo a {
34 | text-decoration: none;
35 | color: #df846c;
36 | font-size: 24px;
37 | }
38 |
39 | /*mobile nav*/
40 | .mobile-menu-btn {
41 | display: block;
42 | position: absolute;
43 | right: 20px;
44 | top: 50%;
45 | transform: translateY(-50%);
46 |
47 | }
48 |
49 | .mobile-menu-btn button {
50 | margin-top: 2px;
51 | padding: 9px 10px;
52 | background: transparent;
53 | border: none;
54 | cursor: pointer;
55 | outline: none;
56 | }
57 |
58 | .btn-bar {
59 | background: #999;
60 | display: block;
61 | width: 22px;
62 | height: 2px;
63 | margin-bottom: 4px;
64 | border-radius: 1px;
65 | }
66 |
67 | .mobile-menu-btn button:hover > .btn-bar {
68 | background: #666;
69 | }
70 |
71 | .mobile-menu-btn button.mobile-user {
72 | padding: 5px;
73 | top: -1px;
74 | right: 40px;
75 | position: absolute;
76 | color: #999;
77 | font-size: 20px;
78 | }
79 |
80 | .mobile-menu-btn button.mobile-user:hover {
81 | color: #666;
82 | }
83 |
84 | .mobile-site-nav {
85 | display: none;
86 | margin-top: 25px;
87 | padding-top: 20px;
88 | padding-bottom: 20px;
89 | background: rgba(255, 255, 255, .6);
90 | border-bottom: 1px solid rgba(204, 204, 204, 0.5);
91 | }
92 |
93 | .mobile-site-nav ul {
94 | display: block;
95 | margin: 0;
96 | padding: 0;
97 | list-style: none;
98 | }
99 |
100 | .mobile-nav-item {
101 | display: block;
102 | text-decoration: none;
103 | color: #555;
104 | padding: 6px 25px;
105 | }
106 |
107 | .mobile-nav-item-active,.mobile-nav-item:hover,.mobile-drop-item:hover {
108 | background: rgba(238, 238, 238, 0.8);
109 | color: #222;
110 | }
111 |
112 | .mobile-drop-item {
113 | text-decoration: none;
114 | color: #666;
115 | padding: 5px 60px;
116 | display: block;
117 | }
118 |
119 | /*mobile search nav*/
120 | .mobile-search {
121 | color: #555;
122 | padding: 6px 25px;
123 | }
124 |
125 | .mobile-search i {
126 | float: left;
127 | }
128 |
129 | .mobile-search input {
130 | border: none;
131 | outline: none;
132 | padding: 0 5px;
133 | height: 18px;
134 | background: rgba(255, 255, 255, .6);
135 | color: #555;
136 | }
137 |
138 | /*mobile sidebar*/
139 | .sidebar {
140 | display: none;
141 | float: left;
142 | width: 260px;
143 | background: #fff;
144 | border-right: 2px solid #ccc;
145 | margin-top: 0;
146 | z-index: 9999;
147 | position: fixed;
148 | overflow-y: auto;
149 | top: 0;
150 | left: 0;
151 | bottom: 0;
152 | }
153 |
154 | .sidebar .side-nav,.sidebar .profile,.sidebar .post-toc,.sidebar .blog-tags {
155 | width: 240px;
156 | }
157 |
158 | /*mobile main*/
159 | .main {
160 | width: 100%;
161 | margin-top: 70px;
162 | }
163 |
164 | span.post-comments-count {
165 | border-right: none;
166 | }
167 |
168 | /*footer*/
169 |
170 | .footer {
171 | background: #fff;
172 | width: 100%;
173 | }
174 |
175 | /*mobile category page*/
176 | .category-post ul {
177 | list-style: none;
178 | margin: 0;
179 | padding: 0;
180 | }
181 |
182 | /*mobile achives page*/
183 | .archives-posts ul {
184 | padding: 0;
185 | }
186 |
187 | /*mobile comment*/
188 | #comments {
189 | font-size: 10px;
190 | color: #999;
191 | padding: 10px;
192 | padding-bottom: 20px;
193 | }
194 |
195 | .textarea-container {
196 | padding: 0;
197 | }
198 |
199 | .textarea-container div.input-div {
200 | width: 100%;
201 | }
202 |
203 | .textarea-container input {
204 | float: none;
205 | }
206 |
207 | ol.comment-list,ul.comment-children {
208 | padding: 0;
209 | }
210 |
211 | .comment-content {
212 | margin-left: 45px;
213 | }
214 |
215 | .comment-children .comment-content {
216 | margin-left: 35px;
217 | }
218 |
219 | /*mobile shuoshuo page*/
220 | .shuo-content span.date-time {
221 | display: none;
222 | }
223 |
224 | .shuo-content article section aside {
225 | margin-left: 8%;
226 | }
227 |
228 | .point-time {
229 | left: 1%;
230 | }
231 |
232 | .shuo-content article section:before {
233 | left: 1%;
234 | }
235 |
236 | .shuo-content article section .brief span.mobile-date-time {
237 | display: inline-block;
238 | }
239 |
240 | /*sticky*/
241 | .sticky {
242 | position: absolute!important;
243 | top: 0!important;
244 | width: 240px!important;
245 | }
246 |
--------------------------------------------------------------------------------
/yublog/static/font/fontello.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yokonsan/yublog/5c61703c283349cb30f2432cc3fb8f141be6800c/yublog/static/font/fontello.eot
--------------------------------------------------------------------------------
/yublog/static/font/fontello.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yokonsan/yublog/5c61703c283349cb30f2432cc3fb8f141be6800c/yublog/static/font/fontello.ttf
--------------------------------------------------------------------------------
/yublog/static/font/fontello.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yokonsan/yublog/5c61703c283349cb30f2432cc3fb8f141be6800c/yublog/static/font/fontello.woff
--------------------------------------------------------------------------------
/yublog/static/font/fontello.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yokonsan/yublog/5c61703c283349cb30f2432cc3fb8f141be6800c/yublog/static/font/fontello.woff2
--------------------------------------------------------------------------------
/yublog/static/images/404.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yokonsan/yublog/5c61703c283349cb30f2432cc3fb8f141be6800c/yublog/static/images/404.jpg
--------------------------------------------------------------------------------
/yublog/static/images/alipay.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yokonsan/yublog/5c61703c283349cb30f2432cc3fb8f141be6800c/yublog/static/images/alipay.jpg
--------------------------------------------------------------------------------
/yublog/static/images/baifeng.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yokonsan/yublog/5c61703c283349cb30f2432cc3fb8f141be6800c/yublog/static/images/baifeng.jpg
--------------------------------------------------------------------------------
/yublog/static/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yokonsan/yublog/5c61703c283349cb30f2432cc3fb8f141be6800c/yublog/static/images/favicon.ico
--------------------------------------------------------------------------------
/yublog/static/images/wechatpay.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yokonsan/yublog/5c61703c283349cb30f2432cc3fb8f141be6800c/yublog/static/images/wechatpay.jpg
--------------------------------------------------------------------------------
/yublog/static/js/admin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by Administrator on 2017/11/21 0021.
3 | */
4 |
5 | function flashEvent() {
6 | // flash message div
7 | let flashDiv = document.getElementsByClassName('flash-msg')[0];
8 | let flashX = document.getElementsByClassName('flash-x')[0];
9 |
10 | flashDiv.style.display = 'none';
11 | }
12 |
13 | function firm(url) {
14 | let request;
15 | if (window.XMLHttpRequest) {
16 | request = new XMLHttpRequest();
17 | } else {
18 | request = new ActiveXObject('Microsoft.XMLHTTP');
19 | }
20 | //利用对话框返回的值 (true 或者 false)
21 | if (confirm("你确定删除吗?")) {
22 | // 发送请求:
23 | request.open('GET', url);
24 | request.send();
25 | location.reload();
26 | }
27 | else { }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/yublog/static/js/comment.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by Administrator on 2017/12/28 0028.
3 | */
4 | (function() {
5 | // form
6 | let commentCon = document.getElementById('comments');
7 | let comForm = commentCon.getElementsByClassName('textarea-container')[0];
8 | let userIpt = comForm.getElementsByTagName('input')[0];
9 | let emailIpt = comForm.getElementsByTagName('input')[1];
10 | let websiteIpt = comForm.getElementsByTagName('input')[2];
11 | let textarea = comForm.getElementsByTagName('textarea')[0];
12 | let authorInfo = commentCon.getElementsByClassName('author-info')[0];
13 | let inputDiv = authorInfo.getElementsByClassName('input-div');
14 | let loginUser = commentCon.getElementsByClassName('logined')[0];
15 | // 判断本地是否有登录记录
16 | let user = ms.get('user');
17 | if (user) {
18 | userIpt.value = user.nickname;
19 | emailIpt.value = user.email;
20 | websiteIpt.value = user.website;
21 | authorInfo.style.display = "none";
22 | loginUser.style.display = "block";
23 | }
24 | let deleteUser = loginUser.getElementsByClassName('delete-user')[0];
25 | if (deleteUser) {
26 | deleteUser.onclick = function() {
27 | ms.remove('user');
28 | loginUser.style.display = "none";
29 | authorInfo.style.display = "block";
30 | };
31 | }
32 | // 本地存储用户
33 | function saveUser(key, val) {
34 | if (!ms.get(user)) {
35 | ms.set(key, val);
36 | }
37 | }
38 | // 回复按钮
39 | let commentIpt = document.getElementsByClassName('comment-send')[0];
40 | let replyBtns = commentCon.getElementsByClassName('comment-reply-link');
41 |
42 | function addOnclick() {
43 | for (let b=0; b speed) {
53 | goSpeed = speed;
54 | } else if (goSpeed < 3) {
55 | goSpeed = 3;
56 | }
57 |
58 | if (top > speed) {
59 | if(document.documentElement.scrollTop){
60 | top = document.documentElement.scrollTop-=speed;
61 | }else{
62 | top = document.body.scrollTop-=speed;
63 | }
64 | } else {
65 | clearInterval(timer);
66 | }
67 | }, 0);
68 | };
69 |
70 | var newTop = document.documentElement.scrollTop || document.body.scrollTop;
71 | if (newTop > 100) {
72 | toTop.style.display = 'block';
73 | } else {
74 | toTop.style.display = 'none';
75 | }
76 | if (newTop > oldTop) {
77 | clearInterval(timer);
78 | }
79 | oldTop = newTop;
80 | };
81 |
82 | // width < 960px
83 | // 导航事件
84 | var mobileNav = document.getElementsByClassName('mobile-site-nav')[0];
85 | var mobileBtnDiv = document.getElementsByClassName('mobile-menu-btn')[0];
86 | var mobileNavBtn = mobileBtnDiv.getElementsByTagName('button')[0];
87 |
88 | clickEvent(mobileNavBtn, mobileNav);
89 |
90 | // 分类显示下拉
91 | var mobileCate = document.getElementsByClassName('mobile-category')[0];
92 | var mobileDrop = document.getElementsByClassName('mobile-drop-category')[0];
93 |
94 | clickEvent(mobileCate, mobileDrop);
95 |
96 | // width < 960px user button
97 | var mobileUserBtn = mobileBtnDiv.getElementsByTagName('button')[1];
98 | var mobileUser = document.getElementsByClassName('sidebar')[0];
99 |
100 | mobileUserBtn.onclick = function() {
101 | if (mobileUser.style.display === "none") {
102 | mobileUser.style.display = "block";
103 | } else if (mobileUser.style.display === "") {
104 | mobileUser.style.display = "block";
105 | } else {
106 | mobileUser.style.display = "none";
107 | }
108 | };
109 |
110 | // love me sidebar
111 | var loveMeTitle = document.getElementsByClassName('love-title')[0];
112 | var loveMe = document.getElementsByClassName('love-me-icon')[0];
113 | var loveMeCount = document.getElementsByClassName('love-me-count')[0];
114 | var counts = parseInt(loveMeCount.innerText);
115 |
116 | var request;
117 | if (window.XMLHttpRequest) {
118 | request = new XMLHttpRequest();
119 | } else {
120 | request = new ActiveXObject('Microsoft.XMLHTTP');
121 | }
122 |
123 | loveMe.onclick = function() {
124 | // 判断本地是否有登录记录
125 | var love = ms.get('love');
126 | if (love) {
127 | loveMeTitle.innerHTML = '我知道你喜欢我';
128 | } else {
129 | ms.set('love', 'loved');
130 | counts += 1;
131 | loveMeTitle.innerHTML = '我也喜欢你';
132 | loveMeCount.innerText = counts.toString();
133 |
134 | // 发送请求:
135 | FormData = JSON.stringify({i_am_handsome: 'yes'});
136 | request.open('POST', '/loveme');
137 | request.setRequestHeader('Content-Type', 'application/json');
138 | request.send(FormData);
139 | }
140 | };
141 |
142 | // 侧栏概览和文章目录的显示
143 | var sideNav = document.getElementsByClassName('side-nav')[0];
144 | var tocBtn = sideNav.getElementsByTagName('span')[0];
145 | var viewBtn = sideNav.getElementsByTagName('span')[1];
146 | var tocBox = document.getElementsByClassName('post-toc')[0];
147 | var viewBox = document.getElementsByClassName('profile')[0];
148 |
149 | tocBtn.onclick = function() {
150 | viewBtn.className = "";
151 | this.className = "current";
152 | viewBox.style.display = "none";
153 | tocBox.style.display = "block";
154 | };
155 | viewBtn.onclick = function() {
156 | tocBtn.className = "";
157 | this.className = "current";
158 | tocBox.style.display = "none";
159 | viewBox.style.display = "block";
160 | };
161 |
162 | })();
163 |
164 |
165 |
--------------------------------------------------------------------------------
/yublog/static/js/myStorage.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by Administrator on 2017/12/30 0030.
3 | */
4 | // 封装localStorage
5 | (function() {
6 | window.ms = {
7 | set: set,
8 | get: get,
9 | remove:remove
10 | };
11 |
12 | function set(key, val) {
13 | localStorage.setItem(key, JSON.stringify(val));
14 | }
15 |
16 | function get(key) {
17 | var json = localStorage.getItem(key);
18 | if (json) {
19 | return JSON.parse(json);
20 | }
21 | }
22 |
23 | function remove(key) {
24 | localStorage.removeItem(key);
25 | }
26 | })();
--------------------------------------------------------------------------------
/yublog/static/js/myToc.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by Administrator on 2018/1/5.
3 | */
4 | (function ($) {
5 | "use strict";
6 |
7 | // Builds a list with the table of contents in the current selector.
8 | // options:
9 | // content: where to look for headings
10 | // headings: string with a comma-separated list of selectors to be used as headings, ordered
11 | // by their relative hierarchy level
12 | var toc = function (options) {
13 | return this.each(function () {
14 | var root = $(this),
15 | data = root.data(),
16 | thisOptions,
17 | stack = [root], // The upside-down stack keeps track of list elements
18 | listTag = this.tagName,
19 | currentLevel = 0,
20 | headingSelectors;
21 |
22 | // Defaults: plugin parameters override data attributes, which override our defaults
23 | thisOptions = $.extend(
24 | {content: "body", headings: "h1,h2,h3"},
25 | {content: data.toc || undefined, headings: data.tocHeadings || undefined},
26 | options
27 | );
28 | headingSelectors = thisOptions.headings.split(",");
29 |
30 | // Set up some automatic IDs if we do not already have them
31 | $(thisOptions.content).find(thisOptions.headings).attr("id", function (index, attr) {
32 | // In HTML5, the id attribute must be at least one character long and must not
33 | // contain any space characters.
34 | //
35 | // We just use the HTML5 spec now because all browsers work fine with it.
36 | // https://mathiasbynens.be/notes/html5-id-class
37 | var generateUniqueId = function (text) {
38 | // Generate a valid ID. Spaces are replaced with underscores. We also check if
39 | // the ID already exists in the document. If so, we append "_1", "_2", etc.
40 | // until we find an unused ID.
41 |
42 | if (text.length === 0) {
43 | text = "?";
44 | }
45 |
46 | var baseId = text.replace(/\s+/g, "_"), suffix = "", count = 1;
47 |
48 | while (document.getElementById(baseId + suffix) !== null) {
49 | suffix = "_" + count++;
50 | }
51 |
52 | return baseId + suffix;
53 | };
54 |
55 | return attr || generateUniqueId($(this).text());
56 | }).each(function () {
57 | // What level is the current heading?
58 | var elem = $(this), level = $.map(headingSelectors, function (selector, index) {
59 | return elem.is(selector) ? index : undefined;
60 | })[0];
61 |
62 | if (level > currentLevel) {
63 | // If the heading is at a deeper level than where we are, start a new nested
64 | // list, but only if we already have some list items in the parent. If we do
65 | // not, that means that we're skipping levels, so we can just add new list items
66 | // at the current level.
67 | // In the upside-down stack, unshift = push, and stack[0] = the top.
68 | var parentItem = stack[0].children("li:last")[0];
69 | if (parentItem) {
70 | stack.unshift($("<" + listTag + "/>").appendTo(parentItem));
71 | }
72 | } else {
73 | // Truncate the stack to the current level by chopping off the 'top' of the
74 | // stack. We also need to preserve at least one element in the stack - that is
75 | // the containing element.
76 | stack.splice(0, Math.min(currentLevel - level, Math.max(stack.length - 1, 0)));
77 | }
78 |
79 | // Add the list item
80 | $("").appendTo(stack[0]).append(
81 | $("").text(elem.text()).attr("href", "#" + elem.attr("id"))
82 | );
83 |
84 | currentLevel = level;
85 | });
86 | });
87 | }, old = $.fn.toc;
88 |
89 | $.fn.toc = toc;
90 |
91 | $.fn.toc.noConflict = function () {
92 | $.fn.toc = old;
93 | return this;
94 | };
95 |
96 | // Data API
97 | $(function () {
98 | toc.call($("[data-toc]"));
99 | });
100 | }(window.jQuery));
101 |
--------------------------------------------------------------------------------
/yublog/static/js/picbed.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by Administrator on 2018/5/15 0015.
3 | */
4 | function showIpt(btn) {
5 | let btn = btn;
6 | let uploadIpt = document.getElementsByClassName('upload-input')[0];
7 | if (btn.innerText === '上传') {
8 | btn.innerText = '取消';
9 | uploadIpt.style.display = 'block';
10 | } else {
11 | btn.innerText = '上传';
12 | uploadIpt.style.display = 'none';
13 | }
14 | }
15 |
16 | function initAjax(method, url, data) {
17 | let request;
18 | if (window.XMLHttpRequest) {
19 | request = new XMLHttpRequest();
20 | } else {
21 | request = new ActiveXObject('Microsoft.XMLHTTP');
22 | }
23 |
24 | request.onreadystatechange = function () { // 状态发生变化时,函数被回调
25 | if (request.readyState === 4) { // 成功完成
26 | // 判断响应结果:
27 | if (request.status === 200) {
28 | // 成功,通过responseText拿到响应的文本:
29 | return success();
30 | } else {
31 | // 失败,根据响应码判断失败原因:
32 | return fail();
33 | }
34 | } else {
35 | // HTTP请求还在继续...
36 | }
37 | };
38 | request.open(method, url);
39 | request.setRequestHeader('Content-Type', 'application/json');
40 | request.send(data);
41 | }
42 |
43 | function renameImg(btn) {
44 | let img = btn.parentNode.parentNode.getElementsByClassName('img-name')[0];
45 | // console.log(img)
46 | let imgKey = img.innerText;
47 | let name=prompt("请输入新的图片名", imgKey);
48 | if (name !== null && name !== "") {
49 | data = JSON.stringify({
50 | key: imgKey,
51 | keyTo: name
52 | });
53 | initAjax('POST', '/admin/qiniu/rename', data);
54 | location.reload();
55 | } else {
56 | // ...
57 | }
58 |
59 | }
60 |
61 | function deleteImg(btn) {
62 | let img = btn.parentNode.parentNode.getElementsByClassName('img-name')[0];
63 | let imgKey = img.innerText;
64 |
65 | if (confirm("你确定删除吗?")) {
66 | data = JSON.stringify({
67 | key: imgKey
68 | });
69 | initAjax('POST', '/admin/qiniu/delete', data);
70 | location.reload();
71 | }
72 | else {
73 | // ...
74 | }
75 | }
76 |
77 |
--------------------------------------------------------------------------------
/yublog/static/lib/default.css:
--------------------------------------------------------------------------------
1 | .highlight .hll { background-color: #ffffcc }
2 | /*.highlight { background: #f8f8f8; }*/
3 | .highlight .c { color: #408080; font-style: italic } /* Comment */
4 | /*.highlight .err { border: 1px solid #FF0000 }*/ /* Error */
5 | .highlight .k { color: #008000; font-weight: bold } /* Keyword */
6 | .highlight .o { color: #666666 } /* Operator */
7 | .highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */
8 | .highlight .cp { color: #BC7A00 } /* Comment.Preproc */
9 | .highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */
10 | .highlight .cs { color: #408080; font-style: italic } /* Comment.Special */
11 | .highlight .gd { color: #A00000 } /* Generic.Deleted */
12 | .highlight .ge { font-style: italic } /* Generic.Emph */
13 | .highlight .gr { color: #FF0000 } /* Generic.Error */
14 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
15 | .highlight .gi { color: #00A000 } /* Generic.Inserted */
16 | .highlight .go { color: #808080 } /* Generic.Output */
17 | .highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
18 | .highlight .gs { font-weight: bold } /* Generic.Strong */
19 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
20 | .highlight .gt { color: #0040D0 } /* Generic.Traceback */
21 | .highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
22 | .highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
23 | .highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
24 | .highlight .kp { color: #008000 } /* Keyword.Pseudo */
25 | .highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
26 | .highlight .kt { color: #B00040 } /* Keyword.Type */
27 | .highlight .m { color: #666666 } /* Literal.Number */
28 | .highlight .s { color: #BA2121 } /* Literal.String */
29 | .highlight .na { color: #7D9029 } /* Name.Attribute */
30 | .highlight .nb { color: #008000 } /* Name.Builtin */
31 | .highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */
32 | .highlight .no { color: #880000 } /* Name.Constant */
33 | .highlight .nd { color: #AA22FF } /* Name.Decorator */
34 | .highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */
35 | .highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
36 | .highlight .nf { color: #0000FF } /* Name.Function */
37 | .highlight .nl { color: #A0A000 } /* Name.Label */
38 | .highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
39 | .highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */
40 | .highlight .nv { color: #19177C } /* Name.Variable */
41 | .highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
42 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */
43 | .highlight .mf { color: #666666 } /* Literal.Number.Float */
44 | .highlight .mh { color: #666666 } /* Literal.Number.Hex */
45 | .highlight .mi { color: #666666 } /* Literal.Number.Integer */
46 | .highlight .mo { color: #666666 } /* Literal.Number.Oct */
47 | .highlight .sb { color: #BA2121 } /* Literal.String.Backtick */
48 | .highlight .sc { color: #BA2121 } /* Literal.String.Char */
49 | .highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
50 | .highlight .s2 { color: #BA2121 } /* Literal.String.Double */
51 | .highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
52 | .highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */
53 | .highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
54 | .highlight .sx { color: #008000 } /* Literal.String.Other */
55 | .highlight .sr { color: #BB6688 } /* Literal.String.Regex */
56 | .highlight .s1 { color: #BA2121 } /* Literal.String.Single */
57 | .highlight .ss { color: #19177C } /* Literal.String.Symbol */
58 | .highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */
59 | .highlight .vc { color: #19177C } /* Name.Variable.Class */
60 | .highlight .vg { color: #19177C } /* Name.Variable.Global */
61 | .highlight .vi { color: #19177C } /* Name.Variable.Instance */
62 | .highlight .il { color: #666666 } /* Literal.Number.Integer.Long */
63 |
--------------------------------------------------------------------------------
/yublog/static/lib/fontello.min.css:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";@font-face{font-family:fontello;src:url(../../font/fontello.eot?32003456);src:url(../../font/fontello.eot?32003456#iefix) format("embedded-opentype"),url(../../font/fontello.woff?32003456) format("woff"),url(../../font/fontello.ttf?32003456) format("truetype"),url(../../font/fontello.svg?32003456#fontello) format("svg");font-weight:400;font-style:normal}[class*=" icon-"]:before,[class^=icon-]:before{font-family:fontello;font-style:normal;font-weight:400;speak:none;display:inline-block;text-decoration:inherit;width:1em;margin-right:.2em;text-align:center;font-variant:normal;text-transform:none;line-height:1em;margin-left:.2em;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-wechat:before{content:'\e800'}.icon-qq:before{content:'\e801'}.icon-bookmark:before{content:'\e802'}.icon-heart:before{content:'\e803'}.icon-mail-alt:before{content:'\e804'}.icon-link:before{content:'\e805'}.icon-phone-1:before{content:'\e806'}
--------------------------------------------------------------------------------
/yublog/static/lib/headroom.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * headroom.js v0.9.4 - Give your page some headroom. Hide your header until you need it
3 | * Copyright (c) 2017 Nick Williams - http://wicky.nillia.ms/headroom.js
4 | * License: MIT
5 | */
6 |
7 | !function(a,b){"use strict";"function"==typeof define&&define.amd?define([],b):"object"==typeof exports?module.exports=b():a.Headroom=b()}(this,function(){"use strict";function a(a){this.callback=a,this.ticking=!1}function b(a){return a&&"undefined"!=typeof window&&(a===window||a.nodeType)}function c(a){if(arguments.length<=0)throw new Error("Missing arguments in extend function");var d,e,f=a||{};for(e=1;ethis.getScrollerHeight();return b||c},toleranceExceeded:function(a,b){return Math.abs(a-this.lastKnownScrollY)>=this.tolerance[b]},shouldUnpin:function(a,b){var c=a>this.lastKnownScrollY,d=a>=this.offset;return c&&d&&b},shouldPin:function(a,b){var c=athis.lastKnownScrollY?"down":"up",c=this.toleranceExceeded(a,b);this.isOutOfBounds(a)||(a<=this.offset?this.top():this.notTop(),a+this.getViewportHeight()>=this.getScrollerHeight()?this.bottom():this.notBottom(),this.shouldUnpin(a,c)?this.unpin():this.shouldPin(a,c)&&this.pin(),this.lastKnownScrollY=a)}},e.options={tolerance:{up:0,down:0},offset:0,scroller:window,classes:{pinned:"headroom--pinned",unpinned:"headroom--unpinned",top:"headroom--top",notTop:"headroom--not-top",bottom:"headroom--bottom",notBottom:"headroom--not-bottom",initial:"headroom"}},e.cutsTheMustard="undefined"!=typeof f&&f.rAF&&f.bind&&f.classList,e});
--------------------------------------------------------------------------------
/yublog/static/robots.txt:
--------------------------------------------------------------------------------
1 | # robots.txt
2 | User-agent: * Allow: /
3 | Allow: /archives/
4 | Disallow: /vendors/
5 | Disallow: /js/
6 | Disallow: /css/
7 | Disallow: /fonts/
8 | Disallow: /vendors/
9 | Disallow: /fancybox/
10 |
11 |
12 | Sitemap: http://www.yukunweb.com/sitemap.xml
13 | Sitemap: http://www.yukunweb.com/baidusitemap.xml
14 |
--------------------------------------------------------------------------------
/yublog/static/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | http://www.yukunweb.com/20210212/qcc-header-signa/
8 | 2021-02-12
9 |
10 |
11 |
12 |
13 | http://www.yukunweb.com/20210212/qcc-header-signsdad/
14 | 2021-02-12
15 |
16 |
17 |
18 |
19 | http://www.yukunweb.com/20210211/qcc-header-sign/
20 | 2021-02-11
21 |
22 |
23 |
--------------------------------------------------------------------------------
/yublog/static/upload/hello-world.md:
--------------------------------------------------------------------------------
1 | ## 介绍
2 |
3 | YuBlog 是一款功能强大且高度自由的个人博客应用。
4 |
5 | ## 常规安装
6 |
7 | ```bash
8 | $ git clone git@github.com:yokonsan/yublog.git # 下载项目
9 | $ cd yublog
10 | $ pip install -r requirements.txt # 安装依赖
11 | ```
12 |
13 | ### 环境准备
14 |
15 | 安装`mysql`数据库,如需启用`redis`缓存,需安装`redis`。
16 |
17 | 缓存选项:
18 | - simple: 使用本地`Python`字典缓存,非线程安全。
19 | - redis: 使用`Redis`作为后端存储缓存值
20 | - filesystem: 使用文件系统来存储缓存值
21 |
22 | 配置文件 [yublog/config.py](yublog/config.py)
23 |
24 | 私密环境变量配置文件 [.env](.env)
25 |
26 |
27 | ### 启动
28 |
29 | ```bash
30 | $ flask init-db # 初始化数据库
31 | $ flask deploy # 生成默认数据
32 | $ flask run # 启动
33 | ```
34 |
35 | 默认地址:[127.0.0.1:5000](http://127.0.0.1:5000)
36 |
37 | 管理后台地址:[127.0.0.1:5000/admin](http://127.0.0.1:5000/admin)
38 |
39 | 账户密码:
40 | ```
41 | 账户:如未配置,默认 yublog
42 | 密码:如未配置,默认 password
43 | ```
44 |
45 | ## Docker
46 |
47 | ### 安装Docker
48 |
49 | 使用官方提供的脚本安装:
50 |
51 | ```bash
52 | $ curl -fsSL get.docker.com -o get-docker.sh
53 | $ sudo sh get-docker.sh --mirror Aliyun
54 | ```
55 |
56 | 执行`$ docker info`命令查看安装是否成功。
57 |
58 | ### 安装docker-compose
59 |
60 | `Compose`项目是 `Docker` 官方的开源项目,负责实现对 `Docker` 容器集群的快速编排。从功能上看,跟 `OpenStack` 中的 `Heat` 十分类似。由于`docker-compose`是使用 Python 编写的,可以直接使用`pip`安装:
61 |
62 | ```bash
63 | $ pip install docker-compose
64 | ```
65 |
66 | ### 启动
67 |
68 | ```bash
69 | $ docker-compose up -d
70 | ```
71 |
72 | 默认地址:[127.0.0.1:9001](http://127.0.0.1:9001)
73 |
74 | 管理后台地址:[127.0.0.1:9001/admin](http://127.0.0.1:9001/admin)
75 |
76 | 账户密码:
77 | ```
78 | 账户:如未配置,默认 yublog
79 | 密码:如未配置,默认 password
80 | ```
81 |
82 | **停止运行:**
83 |
84 | ```bash
85 | $ docker-compose down
86 | ```
87 |
88 | **查看日志:**
89 |
90 | ```bash
91 | $ docker-compose logs # 查看总的容器日志
92 | $ docker-compose logs yublog_web # 查看web应用运行日志
93 | ```
94 |
95 | ## 配置
96 |
97 | 博客运行前需要在`config.py`中配置必须信息。YuBlog 采用`mysql`存储,`redis`做部分缓存,需配置数据库信息。linux 的mysql安装配置可参考上方部署方案。
98 |
99 | `mysql`的配置需根据使用场景选择配置,开发场景配置`DevelopmentConfig`类中的`SQLALCHEMY_DATABASE_URI `的信息。同样的生产环境的配置在`ProductionConfig`类的`SQLALCHEMY_DATABASE_URI `信息。其中默认的`root:password`为`user:password`,`@localhost:3306/db`为`@:/`。
100 |
101 | `redis`的配置为:
102 |
103 | ```python
104 | CACHE_TYPE = 'redis'
105 | CACHE_REDIS_HOST = '127.0.0.1'
106 | CACHE_REDIS_PORT = 6379
107 | CACHE_REDIS_DB = os.getenv('CACHE_REDIS_DB') or ''
108 | CHCHE_REDIS_PASSWORD = os.getenv('CHCHE_REDIS_PASSWORD') or ''
109 | ```
110 |
111 | 其中私密信息建议使用环境变量的形式配置,如`CHCHE_REDIS_PASSWORD`
112 |
113 | ### 防止csrf攻击配置
114 |
115 | ```python
116 | CSRF_ENABLED = True
117 | SECRET_KEY = 'you-guess'
118 | ```
119 |
120 | ### 管理员初始设置
121 |
122 | ```python
123 | # 管理员姓名
124 | ADMIN_NAME = 'yublog'
125 | # 管理员登录信息
126 | ADMIN_LOGIN_NAME = 'yublog'
127 | # 登录密码
128 | ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD') or 'password'
129 | # 博客名
130 | SITE_NAME = 'yublog'
131 | # 博客标题
132 | SITE_TITLE = 'yublog'
133 | # 管理员简介
134 | ADMIN_PROFILE = '克制力,执行力'
135 | ```
136 |
137 | 同样的`ADMIN_PASSWORD`配置建议使用环境变量,初始设置除了登录名和登录密码,可以直接选择默认,可以在管理页面修改。
138 |
139 | ### 站点配置
140 |
141 | ```python
142 | # 发送邮件用户登录
143 | MAIL_USERNAME = os.getenv('MAIL_USERNAME')
144 | # 客户端登录密码非正常登录密码
145 | MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
146 | MAIL_SERVER = os.getenv('MAIL_SERVER') or 'smtp.qq.com'
147 | MAIL_PORT = os.getenv('MAIL_PORT') or '465'
148 |
149 | ADMIN_MAIL_SUBJECT_PREFIX = 'blog'
150 | ADMIN_MAIL_SENDER = 'admin.com'
151 | # 接收邮件通知的邮箱
152 | ADMIN_MAIL = os.getenv('ADMIN_MAIL')
153 |
154 | # 站点搜索的最小字节
155 | WHOOSHEE_MIN_STRING_LEN = 1
156 | ```
157 |
158 | ### 七牛图床配置
159 |
160 | 默认使用本地图床。如需使用七牛云存储作为图床,可将`NEED_PIC_BED`配置为`True`。
161 |
162 | ```python
163 | # 七牛云存储配置
164 | NEED_PIC_BED = False
165 | QN_ACCESS_KEY = os.getenv('QN_ACCESS_KEY') or ''
166 | QN_SECRET_KEY = os.getenv('QN_SECRET_KEY') or ''
167 | # 七牛空间名
168 | QN_PIC_BUCKET = 'bucket-name'
169 | # 七牛外链域名
170 | QN_PIC_DOMAIN = 'domain-url'
171 | ```
172 |
173 | ## 使用
174 |
175 | ### 编辑文章
176 |
177 | 后文对网站的编辑操作,只允许有管理员权限的用户操作。
178 |
179 | `yublog`支持`markdown`语法,上传图片可使用图床进行上传并获取外链。填写说明:
180 |
181 | 
182 |
183 | ```
184 | 分类:技术 # 限制只能写一个分类
185 | 标签:docker,nginx # 标签不限制个数,每个标签之间使用英文的逗号隔开
186 | 链接:nginx-and-docker # 文章的URL,自己可以随意指定,建议有些意义
187 | 日期:2018-08-18 # 年月日间需使用-连接
188 |
189 | 标题:nginx和docker # 文章标题
190 | ```
191 |
192 | 可以选择保存草稿,待下次编辑,也可以直接发布,当然后续更改也很方便。
193 |
194 |
195 | ### 管理文章
196 |
197 | 可以对所有发布过的文章进行更新或者删除。
198 |
199 | ### 审核评论
200 |
201 | 为了防止垃圾评论污染,用户的评论一律需要自己审核通过还是删除。
202 |
203 | ### 管理链接
204 |
205 | 博客支持添加友情链接和社交链接,他们展示在不同的地方。不要搞错了:
206 |
207 | 
208 |
209 | ### 添加专题
210 |
211 | 博客支持系列专题功能,专栏一般是一类文章的聚合,比如系列教程或者日记啥的,文章可以自行选择加密或者不加密。
212 |
213 |
214 | ### 侧栏插件
215 |
216 | 博客支持自定义的侧栏`box`插件:
217 |
218 | 如果想要保持侧栏固定顶部,需要勾选广告选项。插件支持原生的`html,css,js`语法。但要保持宽度不得超过父元素,建议不超过230px。
219 |
220 | ```html
221 |
222 |
223 |
224 | ```
225 |
226 | 前端显示:
227 |
228 | 
229 |
230 | ### 上传文件
231 |
232 | 由于是个人使用,没有对上传的文件做进一步的过滤操作。建议大家不要随意上传`.php`、`.sh`、`.py`的文件。上传的文件位于静态目录:`app/static/upload`下,可以使用`http:///static/upload/`访问。
233 |
234 | ### 图床
235 |
236 | 默认使用本地存储图片。
237 |
238 | 
239 |
240 | #### 七牛云
241 |
242 | 如需使用七牛图床,需要配置好七牛图床的信息。包括个人的`AccessKey/SecretKey`:
243 |
244 | 
245 |
246 | 默认的外链域名为:
247 |
248 | 
249 |
250 | 空间名是个人创建的仓库名。
251 |
252 | 
253 |
254 | 七牛图床主要是为了更方便的管理上传于七牛云的图片,目前支持上传,更名,删除等操作。
255 |
256 |
257 | ## TODO
258 |
259 | - 更加美观的页面;
260 | - 更加人性化的管理后台;
261 | - 优化新评论邮件通知功能;
262 | - 七牛图床批量操作功能;
263 | - 改进评论系统;
264 | - 更简单的部署方式;
265 | - other
266 |
267 |
268 | ## 最后
269 |
270 | 这是我个人使用的博客应用,开始只是用着感觉不错,后来有很多朋友说喜欢,发邮件和我说用的很麻烦,有很多疑问。
271 | 这怪我在写的时候只按自己的喜好,没有想过太多,如果可能我会一直保持对他的改进。
272 |
--------------------------------------------------------------------------------
/yublog/static/upload/image/.gitignore:
--------------------------------------------------------------------------------
1 | # Python:
2 | .idea
3 | .qiniu_pythonsdk_hostscache.json
4 | dist
5 | build
6 | __pycache__
7 | whooshee
8 | migrations
9 | venv
10 | .vscode/
--------------------------------------------------------------------------------
/yublog/static/upload/root.txt:
--------------------------------------------------------------------------------
1 | 9e725139562c0225b6b3b28c2d28ceec
--------------------------------------------------------------------------------
/yublog/templates/_admin_pagination.html:
--------------------------------------------------------------------------------
1 | {% macro pages(pagination, endpoint, fragment='') %}
2 |
29 | {% endmacro %}
--------------------------------------------------------------------------------
/yublog/templates/_comment.html:
--------------------------------------------------------------------------------
1 |
125 |
126 |
--------------------------------------------------------------------------------
/yublog/templates/_pagination.html:
--------------------------------------------------------------------------------
1 | {% macro pages(pagination, cur_page, max_page, endpoint, fragment='') %}
2 |
29 | {% endmacro %}
--------------------------------------------------------------------------------
/yublog/templates/admin/change_password.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block tab %}更改密码{% endblock %}
3 | {% block content %}
4 |
16 | {% endblock %}
--------------------------------------------------------------------------------
/yublog/templates/admin/comment.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% import "_admin_pagination.html" as page %}
3 | {% block tab %}管理评论{% endblock %}
4 | {% block content %}
5 |
30 |
31 |
32 | {{ page.pages(pagination, 'admin.comments') }}
33 |
34 |
35 | {% endblock %}
--------------------------------------------------------------------------------
/yublog/templates/admin/draft.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block tab %}管理草稿{% endblock %}
3 | {% block content %}
4 |
5 | {% if drafts %}
6 |
24 | {% else %}
25 | have no draft
26 | {% endif %}
27 |
28 | {% endblock %}
--------------------------------------------------------------------------------
/yublog/templates/admin/edit_link.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block tab %}添加链接{% endblock %}
3 | {% block content %}
4 |
5 |
6 | 社交链接
7 | 友情链接
8 |
9 |
17 |
18 |
27 |
28 |
29 | {% endblock %}
30 | {% block script %}
31 | {{ super() }}
32 |
52 | {% endblock %}
--------------------------------------------------------------------------------
/yublog/templates/admin/edit_page.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block tab %}添加页面{% endblock %}
3 | {% block content %}
4 |
28 | {% endblock %}
29 |
30 | {% block script %}
31 | {{ super() }}
32 |
33 |
34 |
35 |
44 | {% endblock %}
45 |
--------------------------------------------------------------------------------
/yublog/templates/admin/edit_post.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 |
3 | {% block tab %}编辑文章{% endblock %}
4 | {% block content %}
5 |
37 | {% endblock %}
38 |
39 | {% block script %}
40 | {{ super() }}
41 |
42 |
43 |
44 |
53 | {% endblock %}
54 |
--------------------------------------------------------------------------------
/yublog/templates/admin/edit_sidebox.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block styles %}
3 | {{ super() }}
4 |
16 | {% endblock %}
17 | {% block tab %}编辑插件{% endblock %}
18 | {% block content %}
19 |
20 |
46 | {% endblock %}
47 |
48 |
--------------------------------------------------------------------------------
/yublog/templates/admin/edit_talk.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block tab %}写说说{% endblock %}
3 | {% block content %}
4 |
13 | {% endblock %}
--------------------------------------------------------------------------------
/yublog/templates/admin/index.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block content %}
3 | 你好啊,管理员
4 | 你可以在 这里 阅读使用文档哦
5 | 如果你需要更改博客的样式,或者更改图标,图片啥的,我相信这些不需要我多说
6 | 这里似乎需要写点什么
7 |
8 | 少年听雨歌楼上,红烛昏罗帐。壮年听雨客舟中,江阔云低,断雁叫西风。
9 |
10 | 而今听雨僧庐下,鬓已星星也。悲欢离合总无情,一任阶前,点滴到天明。
11 |
12 | {% endblock %}
--------------------------------------------------------------------------------
/yublog/templates/admin/link.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block tab %}管理链接{% endblock %}
3 | {% block content %}
4 |
5 |
社交链接
6 |
7 | {% for link in social_links %}
8 | -
9 | {{ link.name }}
10 |
11 |
12 |
13 |
{{link.link}}
14 |
15 | {% endfor %}
16 |
17 |
友情链接
18 |
19 | {% for link in friend_links %}
20 | -
21 | {{ link.name }}
22 |
23 |
24 |
25 | {% if link.is_great %}
26 |
27 |
28 |
29 | {% else %}
30 |
31 |
32 |
33 | {% endif %}
34 |
{{link.link}}
35 |
36 | {% endfor %}
37 |
38 |
39 | {% endblock %}
--------------------------------------------------------------------------------
/yublog/templates/admin/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% if title %}{{title}}{%else%}Admin{%endif%} | Admin
6 | {% block styles %}
7 |
8 |
9 | {% endblock %}
10 |
11 |
12 |
13 |
14 | {% with messages = get_flashed_messages() %}
15 | {% if messages %}
16 | {% for message in messages %}
17 |
18 | {{ message }}
19 | x
20 |
21 | {% endfor %}
22 | {% endif %}
23 | {% endwith %}
24 |
25 |
26 |
27 |
你好啊,管理员
28 |
这是一个简单却很安全的登录界面
29 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/yublog/templates/admin/page.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block tab %}管理页面{% endblock %}
3 | {% block content %}
4 |
19 | {% endblock %}
--------------------------------------------------------------------------------
/yublog/templates/admin/post.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% import "_admin_pagination.html" as page %}
3 | {% block tab %}管理文章{% endblock %}
4 | {% block content %}
5 |
25 |
26 |
27 |
28 | {{ page.pages(pagination, 'admin.posts') }}
29 |
30 |
31 | {% endblock %}
--------------------------------------------------------------------------------
/yublog/templates/admin/profile.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block tab %}编辑信息{% endblock %}
3 | {% block content %}
4 |
18 | {% endblock %}
--------------------------------------------------------------------------------
/yublog/templates/admin/sidebox.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block tab %}管理插件{% endblock %}
3 | {% block content %}
4 |
5 |
6 | {% for box in boxes %}
7 | -
8 | {% if box.title %}{{ box.title }}{% else %}无标题{% endif %}
9 |
10 |
11 |
12 |
13 |
14 |
15 | {% if box.unable == True %}
16 |
17 | {% else %}
18 |
19 | {% endif %}
20 |
21 |
{{box.body}}
22 |
23 | {% endfor %}
24 |
25 |
26 | {% endblock %}
--------------------------------------------------------------------------------
/yublog/templates/admin/talk.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block tab %}管理说说{% endblock %}
3 | {% block content %}
4 |
5 |
6 | {% for talk in talks %}
7 | -
8 | {{ talk.strptime }}
9 |
10 |
11 |
12 |
{{talk.talk}}
13 |
14 | {% endfor %}
15 |
16 |
17 | {% endblock %}
--------------------------------------------------------------------------------
/yublog/templates/admin/upload_file.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block tab %}上传文件{% endblock %}
3 | {% block content %}
4 |
5 |
上传的文件访问路径:/xxxx.jpg
6 |
10 |
11 | {% endblock %}
--------------------------------------------------------------------------------
/yublog/templates/admin_base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% if title %}{{title}}{%else%}Admin{%endif%} | Admin
6 | {% block styles %}
7 |
8 |
9 | {% endblock %}
10 |
11 |
12 | {% if current_user.is_authenticated %}
13 |
14 |
72 |
73 |
74 |
主页 /
{% block tab %}{% endblock %}
75 |
76 |
77 |
78 | {% with messages = get_flashed_messages() %}
79 | {% if messages %}
80 | {% for message in messages %}
81 |
82 | {{ message }}
83 | x
84 |
85 | {% endfor %}
86 | {% endif %}
87 | {% endwith %}
88 |
89 | {% block content %}
90 | {% endblock %}
91 |
92 |
93 |
94 | {% block script %}
95 |
96 |
107 | {% endblock %}
108 | {% else %}
109 |
115 | {% endif %}
116 |
117 |
--------------------------------------------------------------------------------
/yublog/templates/admin_column/column.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block tab %}管理专栏{% endblock %}
3 | {% block content %}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
28 |
29 | {% endblock %}
30 |
31 |
--------------------------------------------------------------------------------
/yublog/templates/admin_column/columns.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block tab %}专栏文章{% endblock %}
3 | {% block content %}
4 |
22 | {% endblock %}
23 |
--------------------------------------------------------------------------------
/yublog/templates/admin_column/edit_article.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block tab %}编辑专栏文章{% endblock %}
3 | {% block content %}
4 |
31 | {% endblock %}
32 |
33 | {% block script %}
34 | {{ super() }}
35 |
36 |
37 |
38 |
47 | {% endblock %}
48 |
--------------------------------------------------------------------------------
/yublog/templates/admin_column/edit_column.html:
--------------------------------------------------------------------------------
1 | {% extends "admin_base.html" %}
2 | {% block tab %}编辑专栏{% endblock %}
3 | {% block content %}
4 |
32 | {% endblock %}
33 |
34 | {% block script %}
35 | {{ super() }}
36 |
37 |
38 |
39 |
48 | {% endblock %}
49 |
--------------------------------------------------------------------------------
/yublog/templates/admin_mail.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 消息
9 | |
10 | 你的文章《{{ title }} 》有新留言: |
11 |
12 |
13 |
14 | |
15 |
16 |
17 |
18 | 昵称: {{ nickname }}
19 | 网站: {{ website }}
20 | 邮箱: {{ email }}
21 |
22 | {{ comment | safe}}
23 |
24 | 点击查看完整内容
25 | |
26 |
27 |
28 | Copyright © YuBlog |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/yublog/templates/column/article.html:
--------------------------------------------------------------------------------
1 | {% extends "column_base.html" %}
2 |
3 | {% block nav_btn %}
4 |
5 |
10 |
11 | {% endblock %}
12 |
13 | {% block column_content %}
14 |
33 |
34 |
35 |
36 |
37 |
38 |
{{ article.title }}
39 |
40 |
41 |
42 | {{ article.timestamp }}
43 |
46 | 阅读:
47 |
48 |
49 |
50 |
51 | {{ article.body_to_html | safe }}
52 |
53 |
54 |
55 |
56 | {% if article.prev_article %}
57 |
上一章
58 | {% endif %}
59 | {% if article.next_article %}
60 |
下一章
61 | {% endif %}
62 |
63 |
64 |
65 |
66 |
67 |
68 | {% include "_comment.html" %}
69 |
70 |
71 |
72 | {% endblock %}
73 |
74 | {% block script %}
75 | {{ super() }}
76 |
77 |
113 | {% endblock %}
114 |
--------------------------------------------------------------------------------
/yublog/templates/column/column.html:
--------------------------------------------------------------------------------
1 | {% extends "column_base.html" %}
2 |
3 | {% block column_content %}
4 |
5 |
6 |
7 |
8 |
{{ column.title }}
9 |
10 |
11 |
12 | {{ column.create_time }}
13 |
14 | |
15 |
16 |
17 |
18 |
19 | {{ column.body | safe }}
20 |
26 |
27 |
28 |
29 |
30 |
31 |
主题目录
32 | {% if articles %}
33 | {% for num, item in articles %}
34 |
35 |
{{ num }}
36 |
{{ item.title }}
37 | {% if item.secrecy == True %}
38 |
保密
39 | {% endif %}
40 |
41 | {% endfor %}
42 | {% else %}
43 |
专题还没有文章
44 | {% endif %}
45 |
46 |
47 |
48 | {% endblock %}
49 |
50 | {% block script %}
51 | {{ super() }}
52 |
78 | {% endblock %}
79 |
--------------------------------------------------------------------------------
/yublog/templates/column/enter_password.html:
--------------------------------------------------------------------------------
1 | {% extends "column_base.html" %}
2 | {% block styles %}
3 | {{ super() }}
4 |
18 | {% endblock %}
19 |
20 | {% block column_content %}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
要输入密码才能看哦QAQ~
32 |
33 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | {% endblock %}
47 |
48 |
--------------------------------------------------------------------------------
/yublog/templates/column/index.html:
--------------------------------------------------------------------------------
1 | {% extends "column_base.html" %}
2 |
3 | {% block column_content %}
4 |
5 |
6 | {% for column in columns %}
7 |
8 |
{{ column.title }}
9 |
10 | {{ column.create_time }}
11 |
12 |
13 |
14 | {{ column.body | striptags | truncate(200) }}
15 |
16 |
17 | {% endfor %}
18 |
19 |
20 | {% endblock %}
21 |
22 |
--------------------------------------------------------------------------------
/yublog/templates/column_base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% if title %}{{ title }}{% else %}专栏{% endif %} - 意外
7 |
8 |
9 |
10 |
11 | {% block styles %}
12 |
13 |
14 |
15 |
16 | {% endblock %}
17 |
18 |
19 |
20 |
21 |
37 |
38 |
39 |
40 | {% block column_content %}
41 | {% endblock %}
42 |
43 |
44 |
45 |
56 |
57 |
58 |
59 | {% block script %}
60 |
61 |
62 |
70 | {% endblock %}
71 |
72 |
--------------------------------------------------------------------------------
/yublog/templates/error/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404 | 意外
6 |
23 |
24 |
25 |
26 |
}})
27 |
找不到页面,是不是天然呆打错了。
28 |
回到 首页
29 |
30 |
31 |
--------------------------------------------------------------------------------
/yublog/templates/error/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 500 | 意外
6 |
20 |
21 |
22 |
23 |
服务器又抽风了啦。。。
24 |
管理员赶来修复中。。。
25 |
回到 首页 吧
26 |
27 |
28 |
--------------------------------------------------------------------------------
/yublog/templates/image/index.html:
--------------------------------------------------------------------------------
1 | {% extends "column_base.html" %}
2 |
3 | {% block styles %}
4 | {{ super() }}
5 |
27 | {% endblock %}
28 |
29 | {% block logo_url %}{{ url_for('image.index') }}{% endblock %}
30 |
31 | {% block column_content %}
32 |
33 |
34 |
35 |
本地博客图床
36 |
37 |
38 |
42 |
43 | {% for path in paths %}
44 |
48 | {% endfor %}
49 |
50 |
51 |
52 |
53 | {% endblock %}
54 |
55 | {% block script %}
56 | {{ super() }}
57 |
58 | {% endblock %}
59 |
--------------------------------------------------------------------------------
/yublog/templates/image/path.html:
--------------------------------------------------------------------------------
1 | {% extends "column_base.html" %}
2 | {% block styles %}
3 | {{ super() }}
4 |
89 | {% endblock %}
90 | {% block logo_url %}{{ url_for('image.index') }}{% endblock %}
91 | {% block column_content %}
92 |
93 |
94 |
95 |
96 | 共 {{ images|length }} 张图片
97 |
98 |
99 |
100 |
107 |
108 |
109 |
110 | {% for image in images %}
111 |
112 |
113 |
114 |
115 |
116 |
上传时间:{{ image.timestamp }}
117 |
Markdown引用:
118 |
119 |
120 |
121 |
{{ image.filename }}
122 |
123 |
124 |
125 |
126 |
127 |
128 | {% endfor %}
129 |
130 |
131 |
132 |
133 |
134 | {% endblock %}
135 |
136 | {% block script %}
137 | {{ super() }}
138 |
139 | {% endblock %}
140 |
--------------------------------------------------------------------------------
/yublog/templates/main/archives.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% import "_pagination.html" as page %}
3 | {% block content %}
4 |
5 | 共计 {{ counts }} 篇文章,继续努力哦!
6 |
7 |
8 | {% for y, posts in data.items() %}
9 |
10 | {{ y }}
11 |
12 |
13 |
14 | {% for p in posts %}
15 | -
16 |
17 | {{ p.timestamp }}
18 | {{ p.title }}
19 |
20 | {% endfor %}
21 |
22 |
23 | {% endfor %}
24 |
25 |
26 |
27 | {{ page.pages(pagination, cur_page, max_page, 'main.archives') }}
28 |
29 |
30 | {% endblock %}
--------------------------------------------------------------------------------
/yublog/templates/main/category.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% import "_pagination.html" as page %}
3 | {% block content %}
4 | 分类:{{ category.category }}
5 |
6 |
18 |
19 | {% endblock %}
--------------------------------------------------------------------------------
/yublog/templates/main/friends.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block content %}
3 |
4 |
朋友
5 |
6 | 有朋自远方来,不亦乐乎!
7 |
8 | {% if great_links %}
9 |
时常交流的朋友
10 |
11 |
12 | {% for link in great_links %}
13 | -
14 | {{ link.name }}
15 | {{ link.info }}
16 |
17 | {% endfor %}
18 |
19 |
20 |
21 | {% endif %}
22 |
23 | {% if bad_links %}
24 |
许久不见的朋友
25 |
26 |
27 | {% for link in bad_links %}
28 | -
29 | {{ link.name }}
30 | {{ link.info }}
31 |
32 | {% endfor %}
33 |
34 |
35 |
36 | {% endif %}
37 |
38 | {% endblock %}
39 |
--------------------------------------------------------------------------------
/yublog/templates/main/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {#{% import "_pagination.html" as page %}#}
3 |
4 | {% block styles %}
5 | {{ super() }}
6 |
15 | {% endblock %}
16 |
17 | {% block content %}
18 | {% for post in posts %}
19 |
20 |
25 |
35 |
36 | {{ post.body_to_html | striptags | truncate(250)}}
37 | {#
#}
38 | {#
阅读全文#}
39 | {#
#}
40 |
41 |
42 | {% endfor %}
43 |
44 |
45 |
46 | {# {{ page.pages(pagination, 'main.index') }}#}
47 | -
48 |
49 | «
50 |
51 |
52 | {% for p in pagination %}
53 | {% if p %}
54 | {% if p == cur_page %}
55 | -
56 | {{ p }}
57 |
58 | {% else %}
59 | -
60 | {{ p }}
61 |
62 | {% endif %}
63 | {% else %}
64 | - …
65 | {% endif %}
66 | {% endfor %}
67 | -
68 |
69 | »
70 |
71 |
72 |
73 |
74 | {% endblock %}
75 |
--------------------------------------------------------------------------------
/yublog/templates/main/page.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block styles %}
3 | {{super()}}
4 |
5 |
15 | {% endblock %}
16 | {% block content %}
17 |
18 |
{{page.title}}
19 |
20 |
21 | {% if page.body %}
22 | {{ page.body_to_html | safe }}
23 | {% endif %}
24 |
25 |
26 | {% if page.enable_comment == true %}
27 | {% include "_comment.html" %}
28 | {% endif %}
29 | {% endblock %}
30 |
31 | {% block script %}
32 | {{ super() }}
33 | {% if page.enable_comment == true %}
34 |
35 | {% endif %}
36 |
37 | {% endblock %}
--------------------------------------------------------------------------------
/yublog/templates/main/post.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block styles %}
3 | {{super()}}
4 |
5 |
6 |
7 |
17 | {% endblock %}
18 |
19 | {% block my_plugin %}
20 | {% endblock %}
21 | {% block ads_plugin %}
22 | {% endblock %}
23 |
24 | {% block content %}
25 |
26 |
27 | {{ post.title }}
28 |
29 |
40 |
41 | {% if post.body %}
42 | {{ post.body_to_html | safe }}
43 | {% endif %}
44 |
45 |
46 | {% for tag in post.tags.split(",") %}
47 |
{{ tag }}
48 | {% endfor %}
49 |
50 |
51 |
52 |
53 |
可以请我喝杯咖啡吗QAQ~
54 |
55 |
71 |
72 |
73 |
80 |
81 |
82 | {% if post.prev_post %}
83 |
84 | {{ post.prev_post.title }}
85 |
86 | {% endif %}
87 | {% if post.next_post %}
88 |
89 | {{ post.next_post.title }}
90 |
91 | {% endif %}
92 |
93 |
94 |
95 | {% include "_comment.html" %}
96 |
97 | {% endblock %}
98 |
99 | {% block script %}
100 | {{ super() }}
101 |
102 |
103 |
104 |
122 |
148 | {% endblock %}
149 |
--------------------------------------------------------------------------------
/yublog/templates/main/results.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% import "_pagination.html" as page %}
3 | {% block content %}
4 | {% if results %}
5 | {{query}} 的搜索结果
6 |
20 |
21 |
22 | {{ page.pages(pagination, cur_page, max_page, 'main.search_result', keywords=query) }}
23 |
24 |
25 | {% else %}
26 | 没有 {{query}} 的搜索结果哦!
27 | {% endif %}
28 | {% endblock %}
29 |
--------------------------------------------------------------------------------
/yublog/templates/main/tag.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% import "_pagination.html" as page %}
3 | {% block content %}
4 | 标签:{{ tag }}
5 |
6 |
18 |
19 | {% endblock %}
--------------------------------------------------------------------------------
/yublog/templates/main/talk.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block content %}
4 |
5 | {% if data|length > 0 %}
6 | {% for y, talks in data.items() %}
7 |
8 | {{y}}
9 | {% for t in talks %}
10 |
11 |
12 | {{t.month_and_day}}
13 |
19 |
20 | {% endfor %}
21 |
22 | {% endfor %}
23 | {% else %}
24 |
你好啊,你还没有发过说说哦!
25 | {% endif %}
26 |
27 | {% endblock %}
28 |
--------------------------------------------------------------------------------
/yublog/templates/plugin/picbed.html:
--------------------------------------------------------------------------------
1 | {% extends "admin.html" %}
2 | {% block tab %}七牛图床{% endblock %}
3 | {% block content %}
4 |
5 |
6 | 共 {{ counts }} 张图片
7 |
8 |
9 |
16 |
17 | {% if images %}
18 | {% for item in images %}
19 |
20 |
{{ item.name }}
21 |
22 |
23 |
24 |
25 |
大小:{{ item.size }}kb
26 |
时间:{{ item.time }}
27 |
外链:{{ item.url }}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | {% endfor %}
37 | {% endif %}
38 |
39 |
40 |
41 | {% endblock %}
42 |
43 | {% block script %}
44 | {{ super() }}
45 |
46 | {% endblock %}
47 |
--------------------------------------------------------------------------------
/yublog/templates/user_mail.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | 消息
10 | |
11 |
12 | 您有一条评论回复 |
13 |
14 |
15 |
16 | |
17 |
18 |
19 |
20 | {{ nickname }}, 你好!
21 | 你在该 文章 下
22 | 有新的回复:
23 |
24 | {{ comment | safe }}
25 |
26 | 点击查看完整内容
27 | |
28 |
29 |
30 | Copyright © YuBlog |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/yublog/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yokonsan/yublog/5c61703c283349cb30f2432cc3fb8f141be6800c/yublog/utils/__init__.py
--------------------------------------------------------------------------------
/yublog/utils/as_sync.py:
--------------------------------------------------------------------------------
1 | from os import cpu_count
2 | from functools import wraps
3 | from concurrent.futures import ThreadPoolExecutor
4 |
5 | from loguru import logger
6 | from flask import current_app, _app_ctx_stack, copy_current_request_context # noqa
7 |
8 | pool = ThreadPoolExecutor(max(cpu_count(), 10))
9 |
10 |
11 | def push_app_context(func):
12 | app = current_app._get_current_object() # noqa
13 |
14 | @wraps(func)
15 | def wrapper(*args, **kwargs):
16 | with app.app_context():
17 | return func(*args, **kwargs)
18 |
19 | return wrapper
20 |
21 |
22 | def copy_current_app_context(func):
23 | """retain the app context in the thread pool"""
24 | app_context = _app_ctx_stack.top
25 |
26 | @wraps(func)
27 | def wrapper(*args, **kwargs):
28 | with app_context:
29 | return func(*args, **kwargs)
30 |
31 | return wrapper
32 |
33 |
34 | def as_sync(func):
35 | def log_callback(f):
36 | logger.info(f"Executor over: {func.__name__}")
37 |
38 | e = f.exception()
39 | if e is not None:
40 | logger.error(f"Executor error: {type(e).__name__}: {str(e)}")
41 | return
42 |
43 | @wraps(func)
44 | def wrapper(*args, **kwargs):
45 | pool.submit(func, *args, **kwargs).add_done_callback(log_callback)
46 | return
47 |
48 | return wrapper
49 |
50 |
51 | def sync_copy_app_context(func):
52 | return as_sync(copy_current_app_context(func))
53 |
54 |
55 | def sync_push_app_context(func):
56 | return as_sync(push_app_context(func))
57 |
58 |
59 | def sync_request_context(func):
60 | """retain the app request context in the thread pool"""
61 | return sync_copy_app_context(copy_current_request_context(func))
62 |
--------------------------------------------------------------------------------
/yublog/utils/cache/__init__.py:
--------------------------------------------------------------------------------
1 | from redis import exceptions
2 | from flask import current_app
3 |
4 | from yublog import cache
5 | from yublog.exceptions import NoCacheTypeException
6 | from yublog.utils.as_sync import as_sync
7 | from yublog.utils.log import log_time
8 |
9 |
10 | class CacheKey:
11 | GLOBAL = "global"
12 | ADMIN = "admin"
13 | TAGS = "tags"
14 | CATEGORIES = "categories"
15 | PAGES = "pages"
16 | LOVE_COUNT = "love_count"
17 | POST_COUNT = "post_count"
18 | LAST_TALK = "last_talk"
19 | GUEST_BOOK_COUNT = "guest_book_count"
20 | SOCIAL_LINKS = "social_links"
21 | FRIEND_COUNT = "friend_count"
22 | FRIEND_LINKS = "friend_links"
23 | ADS_BOXES = "ads_boxes"
24 | SITE_BOXES = "site_boxes"
25 | POSTS = "posts"
26 | COLUMNS = "columns"
27 | TALKS = "talks"
28 | IMAGE_PATH = "paths"
29 | IMAGES = "images"
30 | ARTICLES = "articles"
31 |
32 |
33 | class CacheType:
34 | GLOBAL = "global"
35 | POST = "post"
36 | ARTICLE = "article"
37 | COLUMN = "column"
38 | PAGE = "page"
39 | COMMENT = "comment"
40 | TALK = "talk"
41 | LINK = "link"
42 | IMAGE = "image"
43 |
44 |
45 | class Operate:
46 | PREFIX = "_cache"
47 | TYPES = set()
48 |
49 | def join_key(self, typ, key):
50 | """拼接缓存key"""
51 | assert typ in self.TYPES, \
52 | NoCacheTypeException(f"type must in {self.TYPES}, current type[{typ}]")
53 |
54 | return f"{self.PREFIX}:{typ}:{key}".lower()
55 |
56 | def get(self, typ, key):
57 | """查询"""
58 | return cache.get(self.join_key(typ, key))
59 |
60 | def set(self, typ, key, value, timeout=60*60*24*30, **kwargs):
61 | """设置"""
62 | return cache.set(self.join_key(typ, key), value, timeout=timeout, **kwargs)
63 |
64 | def clean(self, typ=None, key="*"):
65 | """删除"""
66 | if typ:
67 | if key != "*":
68 | return cache.delete(self.join_key(typ, key))
69 | # 模糊取所有key
70 |
71 | plugin_prefix = current_app.config["CACHE_KEY_PREFIX"]
72 | keys = cache.cache._read_clients.keys( # noqa
73 | plugin_prefix + self.join_key(typ, key)
74 | )
75 | return cache.delete_many(*[k.decode().lstrip(plugin_prefix) for k in keys])
76 |
77 | return cache.clear()
78 |
79 | def incr(self, typ, key, delta=1):
80 | """数字类型自增"""
81 | try:
82 | cache.cache.inc(self.join_key(typ, key), delta)
83 | except exceptions.ResponseError:
84 | value = int(self.get(typ, key) or 0) + delta
85 | self.set(typ, key, value)
86 |
87 | def decr(self, typ, key, delta=1):
88 | """数字类型自减"""
89 | try:
90 | cache.cache.dec(self.join_key(typ, key), delta)
91 | except exceptions.ResponseError:
92 | value = int(self.get(typ, key) or 0) - delta
93 | self.set(typ, key, value)
94 |
95 | def add(self, typ, key, item):
96 | """保留当前数据,增加缓存数据"""
97 | key = self.join_key(typ, key)
98 | current = cache.get(key)
99 | if isinstance(current, list):
100 | current.append(item)
101 | elif isinstance(current, dict):
102 | current.update(item)
103 |
104 | return cache.set(key, current)
105 |
106 | def get_many(self, typ, *keys):
107 | keys = [self.join_key(typ, key) for key in keys]
108 | return cache.get_many(*keys)
109 |
110 |
111 | class CacheOperate(Operate):
112 | TYPES = [attr.lower() for attr in dir(CacheType) if not attr.startswith("__")]
113 |
114 | @log_time
115 | def getset(self, typ, key, callback=None, **kwargs):
116 | """缓存存在则返回,不存在则设置并返回"""
117 | val = self.get(typ, key)
118 | if not val and callable(callback):
119 | val = callback()
120 | if val:
121 | self.set(typ, key, val, **kwargs)
122 |
123 | return val
124 |
125 |
126 | cache_operate = CacheOperate()
127 |
--------------------------------------------------------------------------------
/yublog/utils/cache/model.py:
--------------------------------------------------------------------------------
1 | from functools import partial
2 |
3 | from yublog.models import Article, Column, Post
4 | from yublog.utils.as_sync import sync_request_context
5 | from yublog.utils.cache import cache_operate, CacheType, CacheKey
6 | from yublog.utils.functools import get_pagination
7 |
8 |
9 | def get_posts():
10 | def filter_by():
11 | posts = []
12 | for p in Post.query\
13 | .filter_by(draft=False)\
14 | .order_by(Post.create_time.desc())\
15 | .all():
16 | p.comment_count = len(comment_cache(p))
17 | posts.append(p)
18 |
19 | return posts
20 |
21 | return cache_operate.getset(
22 | CacheType.POST,
23 | CacheKey.POSTS,
24 | callback=filter_by
25 | )
26 |
27 |
28 | def get_model_cache(typ, key):
29 | """获取博客文章缓存"""
30 | return cache_operate.getset(
31 | typ,
32 | key,
33 | callback=partial(set_model_cache, typ=typ, key=key)
34 | )
35 |
36 |
37 | def set_model_cache(typ, key):
38 | """设置博客文章缓存"""
39 | data = {}
40 | if typ == CacheType.POST:
41 | data = _generate_post_cache(key)
42 | elif typ == CacheType.ARTICLE:
43 | data = _generate_article_cache(key)
44 |
45 | return data
46 |
47 |
48 | def _generate_post_cache(field):
49 | def linked_post(p):
50 | return {
51 | "year": p.year,
52 | "month": p.month,
53 | "url": p.url_name,
54 | "title": p.title
55 | }
56 |
57 | *_, field = field.split("_")
58 | cur = Post.query.filter_by(url_name=field).first_or_404()
59 | posts = get_posts()
60 | cur_idx = posts.index(cur)
61 |
62 | cur.next_post = linked_post(posts[cur_idx+1]) if posts[-1] != cur else None
63 | cur.prev_post = linked_post(posts[cur_idx-1]) if posts[0] != cur else None
64 | cur.comment_count = len(comment_cache(cur))
65 | return cur
66 |
67 |
68 | def _generate_article_cache(field):
69 | def linked_article(a):
70 | return {
71 | "id": a.id,
72 | "title": a.title
73 | }
74 |
75 | cur = Article.query.get_or_404(field)
76 | cur_column = Column.query.get_or_404(cur.column_id)
77 |
78 | articles = cur_column.articles.all()
79 | cur_idx = articles.index(cur)
80 |
81 | cur.next_article = linked_article(articles[cur_idx+1]) if articles[-1] != cur else None
82 | cur.prev_article = linked_article(articles[cur_idx-1]) if articles[0] != cur else None
83 | return cur
84 |
85 |
86 | def comment_cache(model):
87 | def filter_by():
88 | comments = getattr(model, "comments") # 直接抛异常
89 | if comments:
90 | comments = [{c: c.replies} for c in comments if c.disabled and c.replied_id is None]
91 | comments.sort(key=lambda item: list(item.keys())[0], reverse=True)
92 | return comments or []
93 |
94 | return cache_operate.getset(
95 | CacheType.COMMENT,
96 | f"{model.__tablename__}:{model.id}",
97 | callback=filter_by
98 | )
99 |
100 |
101 | def column_cache(url_name):
102 | return cache_operate.getset(
103 | CacheType.COLUMN,
104 | url_name,
105 | callback=lambda: Column.query
106 | .filter_by(url_name=url_name)
107 | .first_or_404()
108 | )
109 |
110 |
111 | def articles_cache(column_id):
112 | return cache_operate.getset(
113 | CacheType.COLUMN,
114 | f"{column_id}:articles",
115 | callback=lambda: Article.query
116 | .filter_by(column_id=column_id)
117 | .order_by(Article.id.desc())
118 | .all()
119 | )
120 |
121 |
122 | def comment_pagination_kwargs(model, cur_page, per):
123 | comments = comment_cache(model)
124 |
125 | counts = len(comments)
126 | max_page, cur_page = get_pagination(counts, per, cur_page)
127 | start_idx = per * (cur_page - 1)
128 | comments = comments[start_idx:start_idx + per]
129 |
130 | return dict(
131 | comments=comments,
132 | counts=counts,
133 | max_page=max_page,
134 | cur_page=cur_page,
135 | pagination=range(1, max_page + 1)
136 | )
137 |
138 |
139 | def post_pagination_kwargs(cur_page, per):
140 | posts = get_posts()
141 |
142 | counts = len(posts)
143 | max_page, cur_page = get_pagination(counts, per, cur_page)
144 | start_idx = per * (cur_page - 1)
145 | posts = [
146 | get_model_cache(CacheType.POST, f"{p.year}_{p.month}_{p.url_name}")
147 | for p in posts[start_idx:start_idx + per]
148 | ]
149 |
150 | return dict(
151 | posts=posts,
152 | counts=counts,
153 | max_page=max_page,
154 | cur_page=cur_page,
155 | pagination=range(1, max_page + 1)
156 | )
157 |
158 |
159 | def clean_post_relative_cache():
160 | @sync_request_context
161 | def _clean_post_relative_cache():
162 | cache_operate.clean(CacheType.POST)
163 | cache_operate.clean(CacheType.GLOBAL, CacheKey.TAGS)
164 | cache_operate.clean(CacheType.GLOBAL, CacheKey.CATEGORIES)
165 | cache_operate.incr(CacheType.GLOBAL, CacheKey.POST_COUNT)
166 |
167 | return _clean_post_relative_cache()
168 |
--------------------------------------------------------------------------------
/yublog/utils/comment.py:
--------------------------------------------------------------------------------
1 | from flask import render_template, current_app, url_for
2 |
3 | from yublog import db, cache_operate, CacheType, CacheKey
4 | from yublog.models import Comment, Post, Page, Article
5 | from yublog.utils.as_sync import sync_request_context
6 | from yublog.utils.emails import send_mail
7 | from yublog.utils.validators import regular_url, format_url
8 |
9 |
10 | class CommentUtils(object):
11 | COMMENT_TARGET_TYPE = {
12 | "post": Post,
13 | "page": Page,
14 | "article": Article
15 | }
16 |
17 | def __init__(self, target_type, form):
18 | self._target_type = target_type
19 | self._data = self.comment_data(form)
20 |
21 | @staticmethod
22 | def comment_data(form):
23 | return {
24 | "nickname": form["nickname"],
25 | "email": form["email"],
26 | "website": form.get("website", None),
27 | "body": form["comment"],
28 | "reply_to": form.get("replyTo", ""),
29 | "reply_author": form.get("replyName", "")
30 | }
31 |
32 | def _generate_comment(self):
33 | website = format_url(self._data["website"])
34 | nickname = self._data["nickname"]
35 | body = self._data["body"]
36 | email = self._data["email"]
37 | comment = Comment(
38 | comment=body,
39 | author=nickname,
40 | email=email,
41 | website=website
42 | )
43 | if self._data["reply_to"]:
44 | reply_author = self._data["reply_author"]
45 | if website and regular_url(website):
46 | nickname_p_tag = f""
48 | comment_html = f"\n\n {body}"
50 | else:
51 | comment_html = f"\n\n{body}"
53 |
54 | comment = Comment(
55 | comment=comment_html,
56 | author=nickname,
57 | email=email,
58 | website=website
59 | )
60 | comment.comment = comment_html
61 | comment.replied_id = int(self._data["reply_to"])
62 | self._data["body"] = comment_html
63 |
64 | return comment
65 |
66 | def save_comment(self, target_id):
67 | if self._target_type not in self.COMMENT_TARGET_TYPE:
68 | current_app.logger.warning("评论保存失败:未获取到目标类型")
69 | return None
70 |
71 | target = self.COMMENT_TARGET_TYPE[self._target_type].query.get_or_404(target_id)
72 | url = self._get_comment_post(target)
73 | if not url:
74 | current_app.logger.warning("评论保存失败:未获取到目标url")
75 | return self._data
76 |
77 | # 生成评论
78 | comment = self._generate_comment()
79 | setattr(comment, self._target_type, target)
80 |
81 | db.session.add(comment)
82 | db.session.commit()
83 | return self._data
84 |
85 | @staticmethod
86 | def _get_comment_post(target):
87 | if isinstance(target, Post):
88 | return url_for("main.post", year=target.year, month=target.month, post_url=target.url_name)
89 |
90 | if isinstance(target, Page):
91 | return url_for("main.page", page_url=target.url_name)
92 |
93 | if isinstance(target, Article):
94 | return url_for("column.article", url_name=target.column.url_name, id=target.id)
95 |
96 | return None
97 |
98 | def _send_mail(self, title, url):
99 | to_mail_address = current_app.config.get("ADMIN_MAIL", "")
100 | if self._data["email"] != to_mail_address:
101 | msg = render_template(
102 | "admin_mail.html",
103 | nickname=self._data["nickname"],
104 | title=title,
105 | comment=self._data["body"],
106 | email=self._data["email"],
107 | website=self._data["website"],
108 | url=url
109 | )
110 | send_mail(to_mail_address, msg)
111 |
112 |
113 | def get_comment_url(comment):
114 | base_url = current_app.config["WEB_URL"]
115 | url = ""
116 | if post := comment.post:
117 | path = [base_url, post.year, post.month, post.url_name]
118 | url = f"https://{'/'.join(str(i) for i in path)}"
119 | elif page := comment.page:
120 | url = f"https://{base_url}/page/{page.url_name}"
121 | elif article := comment.article:
122 | url = f"https://{base_url}/column/{article.column.url_name}/{article.id}"
123 |
124 | return url
125 |
126 |
127 | def update_comment_cache(comment, is_incr=True):
128 | if page := comment.page:
129 | cache_operate.clean(CacheType.PAGE, page.url_name)
130 | cache_operate.clean(CacheType.COMMENT, f"page:{page.id}")
131 | if page.url_name == "guest-book":
132 | (cache_operate.incr if is_incr else cache_operate.decr)(
133 | CacheType.GLOBAL, CacheKey.GUEST_BOOK_COUNT
134 | )
135 |
136 | elif (post := comment.post) and isinstance(post, Post):
137 | cache_operate.clean(
138 | CacheType.POST, f"{post.year}_{post.month}_{post.url_name}"
139 | )
140 | cache_operate.clean(CacheType.COMMENT, f"post:{post.id}")
141 |
142 |
143 | def commit_comment(typ, form, tid):
144 | @sync_request_context
145 | def _commit_comment():
146 | CommentUtils(typ, form).save_comment(tid)
147 |
148 | return _commit_comment()
149 |
--------------------------------------------------------------------------------
/yublog/utils/commit.py:
--------------------------------------------------------------------------------
1 | from yublog.extensions import db
2 |
3 |
4 | def commit(func):
5 | def wrapper(*args, **kwargs):
6 | func(*args, **kwargs)
7 | db.session.commit()
8 |
9 | return wrapper
10 |
11 |
12 | @commit
13 | def add(model):
14 | db.session.add(model)
15 |
16 |
17 | @commit
18 | def delete(model):
19 | db.session.delete(model)
20 |
--------------------------------------------------------------------------------
/yublog/utils/emails.py:
--------------------------------------------------------------------------------
1 | import smtplib
2 | from email.header import Header
3 | from email.mime.text import MIMEText
4 |
5 | from flask import current_app
6 |
7 | from yublog.utils.as_sync import sync_request_context
8 |
9 |
10 | def send_mail(to_addr, msg):
11 | @sync_request_context
12 | def _send():
13 | mail_username = current_app.config['MAIL_USERNAME']
14 | mail_password = current_app.config['MAIL_PASSWORD']
15 | mail_server = current_app.config['MAIL_SERVER']
16 | mail_port = current_app.config['MAIL_PORT']
17 | if not (mail_username and mail_password):
18 | current_app.logger.warning('无邮箱配置,邮件发送失败。')
19 | return
20 |
21 | content = MIMEText(msg, 'html', 'utf-8')
22 | content['Subject'] = Header('新的评论', 'utf-8').encode()
23 | server = smtplib.SMTP_SSL(mail_server, mail_port)
24 | server.login(mail_username, mail_password)
25 | server.sendmail(mail_username, [to_addr], content.as_string())
26 | server.quit()
27 |
28 | return _send()
29 |
--------------------------------------------------------------------------------
/yublog/utils/functools.py:
--------------------------------------------------------------------------------
1 | from yublog.utils.validators import valid_page_num
2 |
3 |
4 | def get_pagination(counts, per, cur_page):
5 | max_page = counts // per + 1 if not counts or counts % per else counts // per
6 | cur_page = valid_page_num(cur_page, max_page)
7 |
8 | return max_page, cur_page
9 |
--------------------------------------------------------------------------------
/yublog/utils/html.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from flask import current_app, url_for
4 | from markdown import Markdown
5 |
6 |
7 | def md2html(body):
8 | """解析markdown"""
9 | if not body:
10 | return ''
11 |
12 | md = Markdown(extensions=[
13 | 'fenced_code',
14 | 'codehilite(css_class=highlight,linenums=None)',
15 | 'admonition',
16 | 'tables',
17 | 'extra'
18 | ])
19 | content = md.convert(body)
20 |
21 | return content
22 |
23 |
24 | def get_sitemap(posts):
25 | """拼接站点地图"""
26 | if not posts:
27 | return None
28 |
29 | header = """
30 |
31 | """
32 | footer, body = "", ""
33 | for post in posts:
34 | content = f"""
35 |
36 | {url_for('main.post', year=post.year, month=post.month, post_url=post.url_name)}/
37 | {post.create_time}
38 |
39 | """
40 | body += f"\n{content}"
41 |
42 | return "\n".join([header, body, footer])
43 |
44 |
45 | def save_file(sitemap, file):
46 | """保存xml文件到静态文件目录"""
47 | filename = os.path.join(os.getcwd(), "yublog", "static", file)
48 | if os.path.exists(filename):
49 | os.remove(filename)
50 |
51 | with open(filename, "w", encoding="utf-8") as f:
52 | f.write(sitemap)
53 | return True
54 |
55 |
56 | def gen_rss_xml(update_time, posts):
57 | """生成 rss xml"""
58 | if not posts:
59 | return None
60 |
61 | # 配置参数
62 | name = current_app.config["ADMIN_NAME"]
63 | title = current_app.config["SITE_NAME"]
64 | subtitle = current_app.config["SITE_TITLE"]
65 | protocol = current_app.config["WEB_PROTOCOL"]
66 | url = current_app.config["WEB_URL"]
67 | web_time = current_app.config["WEB_START_TIME"]
68 | header = f"""
69 |
70 | {title}
71 | {subtitle}
72 |
73 |
74 | tag:{url},{web_time}://1
75 | {update_time}T00:00:00Z
76 | YuBlog
77 | """
78 | body, footer = "", ""
79 | for p in posts:
80 | body += f"""
81 |
82 | {p.title}
83 |
84 | tag:{url},{p.year}://1.{p.id}
85 | {p.create_time}T00:00:00Z
86 | {update_time}T00:00:00Z
87 | {p.title}
88 |
89 | {name}
90 | {protocol}://{url}
91 |
92 |
93 |
94 | \n
95 | """
96 |
97 | return "\n".join([header, body, footer])
98 |
99 |
--------------------------------------------------------------------------------
/yublog/utils/image.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from yublog.utils.as_sync import as_sync
4 |
5 | IMAGE_MIMES = [
6 | 'image/x-icon',
7 | 'image/svg+xml',
8 | 'image/jpeg',
9 | 'image/gif',
10 | 'image/png',
11 | 'image/webp'
12 | ]
13 |
14 |
15 | @as_sync
16 | def mkdir(path):
17 | os.mkdir(path)
18 |
19 |
20 | @as_sync
21 | def saver(path, name, img_stream):
22 | with open(os.path.join(path, name), 'wb') as w:
23 | w.write(img_stream)
24 | return True
25 |
26 |
27 | @as_sync
28 | def remove(path, filename):
29 | os.remove(os.path.join(path, filename))
30 |
31 |
32 | @as_sync
33 | def rename(path, old_name, new_name):
34 | os.rename(os.path.join(path, old_name), os.path.join(path, new_name))
35 |
--------------------------------------------------------------------------------
/yublog/utils/log.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 | import time
3 |
4 | from loguru import logger
5 |
6 | from yublog.utils.times import time_ms
7 |
8 |
9 | def log_time(func):
10 | def wrapped(*args, **kwargs):
11 | start = time_ms()
12 | result = func(*args, **kwargs)
13 | end = time_ms()
14 | logger.debug(f"{func.__name__} executed in {end - start} ms")
15 | return result
16 |
17 | return wrapped
18 |
19 |
20 | def log_param(*, entry=True, exit=True, level="DEBUG"):
21 | """logging of parameters and return
22 |
23 | Args:
24 | entry: record parameters
25 | exit: record return
26 | level: logger level
27 | """
28 | def wrapper(func):
29 | @wraps(func)
30 | def wrapped(*args, **kwargs):
31 | logger_ = logger.opt(depth=1)
32 | if entry:
33 | logger_.log(level, f"{func.__name__}({args, kwargs})")
34 |
35 | result = func(*args, **kwargs)
36 | if exit:
37 | logger_.log(level, f"{func.__name__} return {result}")
38 | return result
39 |
40 | return wrapped
41 |
42 | return wrapper
43 |
--------------------------------------------------------------------------------
/yublog/utils/pxfilter.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import re
5 | from abc import ABC
6 |
7 | from html.parser import HTMLParser
8 |
9 |
10 | class XssHtml(HTMLParser, ABC):
11 | """xss"""
12 | allow_tags = ['a', 'img', 'br', 'strong', 'b', 'code', 'pre',
13 | 'p', 'div', 'em', 'span', 'h1', 'h2', 'h3', 'h4',
14 | 'h5', 'h6', 'blockquote', 'ul', 'ol', 'tr', 'th', 'td',
15 | 'hr', 'li', 'u', 'embed', 's', 'table', 'thead', 'tbody',
16 | 'caption', 'small', 'q', 'sup', 'sub']
17 | common_attrs = ["style", "class", "name"]
18 | nonend_tags = ["img", "hr", "br", "embed"]
19 | tags_own_attrs = {
20 | "img": ["src", "width", "height", "alt", "align"],
21 | "a": ["href", "target", "rel", "title"],
22 | "embed": ["src", "width", "height", "type", "allowfullscreen", "loop", "play", "wmode", "menu"],
23 | "table": ["border", "cellpadding", "cellspacing"],
24 | }
25 |
26 | _regex_url = re.compile(r'^(http|https|ftp)://.*', re.I | re.S)
27 | _regex_style_1 = re.compile(r'(\\||/\*|\*/)', re.I)
28 | _regex_style_2 = re.compile(r'e.*x.*p.*r.*e.*s.*s.*i.*o.*n', re.I | re.S)
29 |
30 | def __init__(self, allows=None):
31 | HTMLParser.__init__(self)
32 | self.allow_tags = allows if allows and isinstance(allows, list) else self.allow_tags
33 | self.result = []
34 | self.start = []
35 | self.data = []
36 |
37 | def get_html(self):
38 | """
39 | Get the safe html code
40 | """
41 | for i in range(0, len(self.result)):
42 | self.data.append(self.result[i])
43 | return ''.join(self.data)
44 |
45 | def handle_startendtag(self, tag, attrs):
46 | self.handle_starttag(tag, attrs)
47 |
48 | def handle_starttag(self, tag, attrs):
49 | if tag not in self.allow_tags:
50 | return
51 | end_diagonal = ' /' if tag in self.nonend_tags else ''
52 | if not end_diagonal:
53 | self.start.append(tag)
54 | attdict = {}
55 | for attr in attrs:
56 | attdict[attr[0]] = attr[1]
57 |
58 | attdict = self._wash_attr(attdict, tag)
59 | if hasattr(self, "node_{}".format(tag)):
60 | attdict = getattr(self, "node_{}".format(tag))(attdict)
61 | else:
62 | attdict = self.node_default(attdict)
63 |
64 | attrs = []
65 | for (key, value) in attdict.items():
66 | attrs.append('{0}="{1}"'.format(key, self._htmlspecialchars(value)))
67 | attrs = (' {}'.format(' '.join(attrs))) if attrs else ''
68 | self.result.append('<{0}{1}{2}>'.format(tag, attrs, end_diagonal))
69 |
70 | def handle_endtag(self, tag):
71 | if self.start and tag == self.start[len(self.start) - 1]:
72 | self.result.append('{}>'.format(tag))
73 | self.start.pop()
74 |
75 | def handle_data(self, data):
76 | self.result.append(self._htmlspecialchars(data))
77 |
78 | def handle_entityref(self, name):
79 | if name.isalpha():
80 | self.result.append("&{};".format(name))
81 |
82 | def handle_charref(self, name):
83 | if name.isdigit():
84 | self.result.append("{};".format(name))
85 |
86 | def node_default(self, attrs):
87 | attrs = self._common_attr(attrs)
88 | return attrs
89 |
90 | def node_a(self, attrs):
91 | attrs = self._common_attr(attrs)
92 | attrs = self._get_link(attrs, "href")
93 | attrs = self._set_attr_default(attrs, "target", "_blank")
94 | attrs = self._limit_attr(attrs, {
95 | "target": ["_blank", "_self"]
96 | })
97 | return attrs
98 |
99 | def node_embed(self, attrs):
100 | attrs = self._common_attr(attrs)
101 | attrs = self._get_link(attrs, "src")
102 | attrs = self._limit_attr(attrs, {
103 | "type": ["application/x-shockwave-flash"],
104 | "wmode": ["transparent", "window", "opaque"],
105 | "play": ["true", "false"],
106 | "loop": ["true", "false"],
107 | "menu": ["true", "false"],
108 | "allowfullscreen": ["true", "false"]
109 | })
110 | attrs["allowscriptaccess"] = "never"
111 | attrs["allownetworking"] = "none"
112 | return attrs
113 |
114 | def _true_url(self, url):
115 | if self._regex_url.match(url):
116 | return url
117 | else:
118 | return "http://{}".format(url)
119 |
120 | def _true_style(self, style):
121 | if style:
122 | style = self._regex_style_1.sub('_', style)
123 | style = self._regex_style_2.sub('_', style)
124 | return style
125 |
126 | def _get_style(self, attrs):
127 | if "style" in attrs:
128 | attrs["style"] = self._true_style(attrs.get("style"))
129 | return attrs
130 |
131 | def _get_link(self, attrs, name):
132 | if name in attrs:
133 | attrs[name] = self._true_url(attrs[name])
134 | return attrs
135 |
136 | def _wash_attr(self, attrs, tag):
137 | if tag in self.tags_own_attrs:
138 | other = self.tags_own_attrs.get(tag)
139 | else:
140 | other = []
141 |
142 | _attrs = {}
143 | if attrs:
144 | for (key, value) in attrs.items():
145 | if key in self.common_attrs + other:
146 | _attrs[key] = value
147 | return _attrs
148 |
149 | def _common_attr(self, attrs):
150 | attrs = self._get_style(attrs)
151 | return attrs
152 |
153 | def _set_attr_default(self, attrs, name, default=''):
154 | if name not in attrs:
155 | attrs[name] = default
156 | return attrs
157 |
158 | def _limit_attr(self, attrs, limit={}):
159 | for (key, value) in limit.items():
160 | if key in attrs and attrs[key] not in value:
161 | del attrs[key]
162 | return attrs
163 |
164 | def _htmlspecialchars(self, html):
165 | return html.replace("<", "<")\
166 | .replace(">", ">")\
167 | .replace('"', """)\
168 | .replace("'", "'")
169 |
170 |
171 | # parser = XssHtml()
172 |
173 | if __name__ == '__main__':
174 | parser = XssHtml()
175 | parser.feed("""
176 |
177 |
178 |
179 |
180 | """)
181 | parser.close()
182 | print(parser.get_html())
183 |
--------------------------------------------------------------------------------
/yublog/utils/save.py:
--------------------------------------------------------------------------------
1 | from flask import current_app
2 |
3 | from yublog.extensions import db
4 | from yublog.models import Tag, Category, Post
5 | from yublog.utils.as_sync import sync_request_context
6 | from yublog.utils.html import get_sitemap, save_file, gen_rss_xml
7 |
8 |
9 | def save_tags(tags):
10 | @sync_request_context
11 | def _save_tags():
12 | for tag in tags:
13 | exist_tag = Tag.query.filter_by(tag=tag).first()
14 | if not exist_tag:
15 | tag = Tag(tag=tag)
16 | db.session.add(tag)
17 | db.session.commit()
18 |
19 | return _save_tags()
20 |
21 |
22 | def save_post(form, is_draft=False):
23 | category = save_category(form.category.data, is_show=not is_draft)
24 |
25 | tags = form.tags.data.split(",")
26 | post = Post(body=form.body.data, title=form.title.data,
27 | url_name=form.url_name.data, category=category,
28 | tags=form.tags.data, create_time=form.create_time.data, draft=is_draft)
29 | if not is_draft:
30 | save_tags(tags)
31 | save_xml(post.create_time)
32 |
33 | return post
34 |
35 |
36 | def save_category(old_category, new_category=None, is_show=True):
37 | if new_category:
38 | category = Category.query.filter_by(category=old_category).first()
39 | if category.posts.count() == 1:
40 | # new one replace old one
41 | db.session.delete(category)
42 | db.session.commit()
43 |
44 | old_category = new_category
45 |
46 | category = Category.query.filter_by(category=old_category).first()
47 | if not category:
48 | # add new category
49 | category = Category(category=old_category, is_show=is_show)
50 | db.session.add(category)
51 | db.session.commit()
52 | return category
53 |
54 |
55 | def save_xml(update_time):
56 | @sync_request_context
57 | def _save_xml():
58 | posts = Post.query.filter_by(draft=False).order_by(Post.create_time.desc()).all()
59 | # sitemap
60 | sitemap = get_sitemap(posts)
61 | save_file(sitemap, "sitemap.xml")
62 | # rss
63 | rss_posts = posts[:current_app.config["RSS_COUNTS"]]
64 | rss = gen_rss_xml(update_time, rss_posts)
65 | save_file(rss, "atom.xml")
66 |
67 | return _save_xml()
68 |
--------------------------------------------------------------------------------
/yublog/utils/times.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import time
3 |
4 |
5 | def time_ms():
6 | return int(time.time() * 1000)
7 |
8 |
9 | def nowstr(fmt="%Y-%m-%d %H:%M:%S"):
10 | t = datetime.datetime.now()
11 | return t.strftime(fmt)
12 |
--------------------------------------------------------------------------------
/yublog/utils/validators.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 |
4 | def valid_page_num(num, maximum, minimum=1):
5 | if num > minimum:
6 | return min(num, maximum)
7 |
8 | elif num < maximum:
9 | return max(num, minimum)
10 |
11 | return num
12 |
13 |
14 | def regular_url(url):
15 | pattern = '^http[s]*?://[\u4e00-\u9fff\w./]+$'
16 | return re.match(pattern, url)
17 |
18 |
19 | def format_url(url):
20 | if not url.startswith(("http://", "https://")):
21 | return f"http://{url}"
22 |
23 | return url
24 |
--------------------------------------------------------------------------------
/yublog/views/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 |
3 |
4 | main_bp = Blueprint("main", __name__)
5 | column_bp = Blueprint("column", __name__)
6 | api_bp = Blueprint("api", __name__)
7 | admin_bp = Blueprint("admin", __name__)
8 | image_bp = Blueprint("image", __name__)
9 |
10 |
11 | from yublog.views import main, admin, column, site, api, error, image # noqa
12 |
--------------------------------------------------------------------------------
/yublog/views/api.py:
--------------------------------------------------------------------------------
1 | from flask import jsonify, request
2 |
3 | from yublog.utils import commit
4 | from yublog.views import api_bp
5 | from yublog.models import View
6 |
7 |
8 | @api_bp.route("/view//", methods=["GET"])
9 | def views(typ, id):
10 | view = View.query.filter_by(type=typ, relationship_id=id).first()
11 | if not view:
12 | view = View(type=typ, count=0, relationship_id=id)
13 |
14 | if request.cookies.get(f"{typ}_{id}"):
15 | return jsonify(count=view.count)
16 |
17 | view.count += 1
18 | commit.add(view)
19 | resp = jsonify(count=view.count)
20 | resp.set_cookie(f"{typ}_{id}", "1", max_age=1 * 24 * 60 * 60)
21 | return resp
22 |
--------------------------------------------------------------------------------
/yublog/views/column.py:
--------------------------------------------------------------------------------
1 | from flask import (
2 | render_template,
3 | request,
4 | jsonify,
5 | current_app,
6 | redirect,
7 | url_for,
8 | make_response,
9 | abort
10 | )
11 |
12 | from yublog import CacheType, cache_operate, CacheKey
13 | from yublog.forms import ArticlePasswordForm
14 | from yublog.models import Column
15 | from yublog.views import column_bp
16 | from yublog.utils.comment import commit_comment
17 | from yublog.utils.cache.model import get_model_cache, comment_pagination_kwargs, column_cache, articles_cache
18 |
19 |
20 | @column_bp.route("/")
21 | def index():
22 | columns = cache_operate.getset(
23 | CacheType.COLUMN,
24 | CacheKey.COLUMNS,
25 | callback=lambda: Column.query
26 | .order_by(Column.id.desc())
27 | .all()
28 | )
29 |
30 | return render_template(
31 | "column/index.html",
32 | title="专栏目录",
33 | columns=columns
34 | )
35 |
36 |
37 | @column_bp.route("/")
38 | def _column(url_name):
39 | column = column_cache(url_name)
40 |
41 | articles = articles_cache(column.id)
42 |
43 | first_id = articles[0].id if articles else None
44 | return render_template(
45 | "column/column.html",
46 | column=column,
47 | title=column.title,
48 | articles=enumerate(articles, 1),
49 | first_id=first_id
50 | )
51 |
52 |
53 | @column_bp.route("//article/")
54 | def article(url_name, id):
55 | column = column_cache(url_name)
56 | articles = articles_cache(column.id)
57 | _article = get_model_cache(CacheType.ARTICLE, id) or abort(404)
58 |
59 | # judge whether secrecy
60 | if _article.secrecy \
61 | and request.cookies.get("secrecy") != column.password_hash:
62 | return redirect(url_for(
63 | "column.enter_password", url_name=url_name, id=id
64 | ))
65 |
66 | per = current_app.config["COMMENTS_PER_PAGE"]
67 | cur_page = max(request.args.get("page", 1, type=int), 1)
68 | comment_args = comment_pagination_kwargs(_article, cur_page, per)
69 |
70 | return render_template(
71 | "column/article.html",
72 | title=_article.title,
73 | column=column,
74 | article=_article,
75 | articles=enumerate(articles, 1),
76 | **comment_args
77 | )
78 |
79 |
80 | @column_bp.route(
81 | "//article//password",
82 | methods=["GET", "POST"]
83 | )
84 | def enter_password(url_name, id):
85 | form = ArticlePasswordForm()
86 | if form.validate_on_submit():
87 | column = column_cache(url_name)
88 | if column.verify_password(form.password.data):
89 | resp = make_response(redirect(url_for(
90 | "column.article", url_name=url_name, id=id
91 | )))
92 | resp.set_cookie(
93 | "secrecy", column.password_hash, max_age=7 * 24 * 60 * 60
94 | )
95 | return resp
96 |
97 | return redirect(url_for(
98 | "column.enter_password", url_name=url_name, id=id
99 | ))
100 | return render_template(
101 | "column/enter_password.html",
102 | title="输入密码",
103 | id=id,
104 | form=form,
105 | url_name=url_name,
106 | )
107 |
108 |
109 | @column_bp.route("/column//comment", methods=["POST"])
110 | def comment(id):
111 | form = request.get_json()
112 |
113 | commit_comment("article", form, id)
114 | return jsonify(**form)
115 |
--------------------------------------------------------------------------------
/yublog/views/error.py:
--------------------------------------------------------------------------------
1 | from flask import render_template
2 |
3 | from yublog.views import main_bp
4 |
5 |
6 | @main_bp.app_errorhandler(404)
7 | def page_not_found(e):
8 | return render_template("error/404.html", title="404"), 404
9 |
10 |
11 | @main_bp.app_errorhandler(500)
12 | def internal_server_error(e):
13 | return render_template("error/500.html", title="500"), 500
14 |
15 |
--------------------------------------------------------------------------------
/yublog/views/image.py:
--------------------------------------------------------------------------------
1 | import re
2 | import os
3 |
4 | from flask import (
5 | render_template,
6 | send_from_directory,
7 | request,
8 | redirect,
9 | url_for
10 | )
11 | from flask.globals import current_app
12 | from flask_login import login_required
13 | from loguru import logger
14 |
15 | from yublog import cache_operate, CacheType, CacheKey
16 | from yublog.forms import AddImagePathForm
17 | from yublog.models import Image, ImagePath
18 | from yublog.views import image_bp
19 | from yublog.utils import image, commit
20 |
21 | PATH_SUB = r'[./\\\'"]'
22 | IMAGE_SUB = r'[/\\\':*?"<>|]'
23 |
24 |
25 | @image_bp.route("/")
26 | @image_bp.route("/index", methods=["GET", "POST"])
27 | @login_required
28 | def index():
29 | _paths = cache_operate.getset(
30 | CacheType.IMAGE,
31 | CacheKey.IMAGE_PATH,
32 | lambda: ImagePath.query.all()
33 | )
34 | paths = {p.path for p in _paths}
35 |
36 | form = AddImagePathForm()
37 | if form.validate_on_submit():
38 | path = re.sub(PATH_SUB, r"_", form.path_name.data)
39 | if path and path not in paths:
40 | commit.add(ImagePath(path=path))
41 | image.mkdir(os.path.join(
42 | current_app.config["IMAGE_UPLOAD_PATH"], path
43 | ))
44 | cache_operate.clean(CacheType.IMAGE, CacheKey.IMAGE_PATH)
45 | logger.info("Add image path successful.")
46 | else:
47 | logger.info("Add image path fail.")
48 |
49 | return redirect(url_for("image.index"))
50 | return render_template(
51 | "image/index.html",
52 | title="图片",
53 | form=form,
54 | paths=paths,
55 | )
56 |
57 |
58 | @image_bp.route("//", methods=["GET", "POST"])
59 | @login_required
60 | def get_path_images(path):
61 | images = cache_operate.getset(
62 | CacheType.IMAGE,
63 | f"{path}:{CacheKey.IMAGES}",
64 | lambda: Image.query.filter_by(path=path).order_by(Image.id).all()
65 | )
66 | filenames = {i.filename for i in images}
67 | if request.method == "POST":
68 | img_name = request.form.get("key")
69 | file = request.files["file"]
70 | filename = file.filename \
71 | if not img_name else re.sub(IMAGE_SUB, r"_", img_name)
72 |
73 | if filename not in filenames and file.mimetype in image.IMAGE_MIMES:
74 | image.saver(
75 | os.path.join(current_app.config["IMAGE_UPLOAD_PATH"], path),
76 | filename,
77 | file.stream.read()
78 | )
79 |
80 | _path = cache_operate.getset(
81 | CacheType.IMAGE,
82 | path,
83 | lambda: ImagePath.query.filter_by(path=path).first()
84 | )
85 | commit.add(Image(path=path, filename=filename, image_path=_path))
86 | logger.info(f"Upload image {filename} successful")
87 | else:
88 | logger.info("Upload image fail")
89 | return redirect(url_for("image.get_path_images", path=path))
90 |
91 | return render_template(
92 | "image/path.html",
93 | title="图片路径",
94 | path=path,
95 | images=images,
96 | )
97 |
98 |
99 | @image_bp.route("//")
100 | def get_image(path, filename):
101 | return send_from_directory(
102 | "static",
103 | f"upload/image/{path}/{filename}"
104 | )
105 |
106 |
107 | @image_bp.route("/delete", methods=["GET", "POST"])
108 | @login_required
109 | def delete_img():
110 | _image = Image.query.get_or_404(request.get_json()["id"])
111 | cur_img_path = _image.path
112 | filename = _image.filename
113 |
114 | commit.delete(_image)
115 | image.remove(
116 | os.path.join(current_app.config["IMAGE_UPLOAD_PATH"], cur_img_path),
117 | filename
118 | )
119 | logger.info("Delete image successful")
120 | cache_operate.clean(CacheType.IMAGE, f"{cur_img_path}:{CacheKey.IMAGES}")
121 | return redirect(url_for("image.get_path_images", path=cur_img_path))
122 |
123 |
124 | @image_bp.route("/rename", methods=["GET", "POST"])
125 | @login_required
126 | def rename_img():
127 | new_name = re.sub(IMAGE_SUB, r"_", request.get_json()["newName"])
128 | _image = Image.query.get_or_404(request.get_json()["id"])
129 | cur_img_path = _image.path
130 |
131 | images = cache_operate.getset(
132 | CacheType.IMAGE,
133 | f"{cur_img_path}:{CacheKey.IMAGES}",
134 | lambda: Image.query.filter_by(path=cur_img_path).order_by(Image.id).all()
135 | )
136 | filenames = {i.filename for i in images}
137 | if new_name in filenames:
138 | return redirect(url_for("image.get_path_images", path=cur_img_path))
139 |
140 | old_name = _image.filename
141 | _image.filename = new_name
142 | commit.add(_image)
143 |
144 | image.rename(
145 | os.path.join(current_app.config["IMAGE_UPLOAD_PATH"], cur_img_path),
146 | old_name,
147 | new_name
148 | )
149 | cache_operate.clean(CacheType.IMAGE, f"{cur_img_path}:{CacheKey.IMAGES}")
150 | logger.info(f"Rename image {new_name} successful")
151 | return redirect(url_for("image.get_path_images", path=cur_img_path))
152 |
--------------------------------------------------------------------------------
/yublog/views/site.py:
--------------------------------------------------------------------------------
1 | from flask import send_from_directory
2 |
3 | from yublog.views import main_bp
4 |
5 |
6 | @main_bp.route("/sitemap.xml")
7 | def sitemap():
8 | return send_from_directory("static", "sitemap.xml")
9 |
10 |
11 | @main_bp.route("/robots.txt")
12 | def robots():
13 | return send_from_directory("static", "robots.txt")
14 |
15 |
16 | @main_bp.route("/atom.xml")
17 | def rss():
18 | return send_from_directory("static", "atom.xml")
19 |
20 |
21 | @main_bp.route("//")
22 | def get_file(filename):
23 | return send_from_directory("static", f"upload/{filename}")
24 |
25 |
26 | @main_bp.route("///")
27 | def get_dir_file(path, filename):
28 | return send_from_directory("static", f"upload/{path}/{filename}")
29 |
30 |
31 | @main_bp.route("/init")
32 | def init():
33 | pass
34 |
--------------------------------------------------------------------------------