├── .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 | ![write](/image/post0/post.png) 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 | ![add-link](/image/post0/add-link.png) 208 | 209 | ### 添加专题 210 | 211 | 博客支持系列专题功能,专栏一般是一类文章的聚合,比如系列教程或者日记啥的,文章可以自行选择加密或者不加密。 212 | 213 | 214 | ### 侧栏插件 215 | 216 | 博客支持自定义的侧栏`box`插件: 217 | 218 | 如果想要保持侧栏固定顶部,需要勾选广告选项。插件支持原生的`html,css,js`语法。但要保持宽度不得超过父元素,建议不超过230px。 219 | 220 | ```html 221 | 222 | yokonsan 223 | 224 | ``` 225 | 226 | 前端显示: 227 | 228 | ![box-demo](/image/post0/box-demo.png) 229 | 230 | ### 上传文件 231 | 232 | 由于是个人使用,没有对上传的文件做进一步的过滤操作。建议大家不要随意上传`.php`、`.sh`、`.py`的文件。上传的文件位于静态目录:`app/static/upload`下,可以使用`http:///static/upload/`访问。 233 | 234 | ### 图床 235 | 236 | 默认使用本地存储图片。 237 | 238 | ![pic-bed](/image/post0/pic-bed-demo.png) 239 | 240 | #### 七牛云 241 | 242 | 如需使用七牛图床,需要配置好七牛图床的信息。包括个人的`AccessKey/SecretKey`: 243 | 244 | ![qiniu](/image/post0/page-qiniu.png) 245 | 246 | 默认的外链域名为: 247 | 248 | ![qiniu2](/image/post0/page-qiniu2.png) 249 | 250 | 空间名是个人创建的仓库名。 251 | 252 | ![qiniu3](/image/post0/page-qiniu3.png) 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 |
      3 |
    • 4 | 5 | « 6 | 7 |
    • 8 | {% for p in pagination.iter_pages() %} 9 | {% if p %} 10 | {% if p == pagination.page %} 11 |
    • 12 | {{ p }} 13 |
    • 14 | {% else %} 15 |
    • 16 | {{ p }} 17 |
    • 18 | {% endif %} 19 | {% else %} 20 |
    • 21 | {% endif %} 22 | {% endfor %} 23 |
    • 24 | 25 | » 26 | 27 |
    • 28 |
    29 | {% endmacro %} -------------------------------------------------------------------------------- /yublog/templates/_comment.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 | {{ counts }} 条评论 5 |
    6 | 7 |
    8 |
      9 | {% if comments %} 10 | {% for _comment in comments %} 11 | {% for comment, replies in _comment.items() %} 12 |
    1. 13 |
      14 |
      15 | author 16 |
      17 | {{ comment.strptime }} 18 |
      19 | 20 | {% if comment.website %} 21 | 22 | {{ comment.author }} 23 | 24 | {% else %} 25 | {{ comment.author }} 26 | {% endif %} 27 | 28 |
      29 | 回复 30 |
      31 |
      32 |

      {{ comment.body_to_html | safe }}

      33 |
      34 |
      35 |
      36 | {% if replies %} 37 |
        38 | {% for reply in replies %} 39 | {% if reply.disabled %} 40 |
      • 41 |
        42 |
        43 | 44 |
        45 | {{ reply.strptime }} 46 |
        47 | {# {{ reply.author }}#} 48 |
        49 | 回复 50 |
        51 |
        52 | {{ reply.body_to_html | safe }} 53 |
        54 |
        55 |
        56 |
      • 57 | {% endif %} 58 | {% endfor %} 59 |
      60 | {% endif %} 61 |
    2. 62 | {% endfor %} 63 | {% endfor %} 64 | {% endif %} 65 |
    66 |
    67 | 68 | {% if max_page != 1 %} 69 |
    70 | 74 | {% for p in pagination %} 75 | {% if p %} 76 | {% if p==cur_page %} 77 | {{ p }} 78 | {% else %} 79 | {{ p }} 80 | {% endif %} 81 | {% else %} 82 | 83 | {% endif %} 84 | {% endfor %} 85 | 87 | 下一页 88 | 89 |
    90 | {% endif %} 91 | 92 |
    93 |
    94 | 95 |
    96 |
    97 | 98 | 99 |
    100 |
    101 | 102 | 103 |
    104 |
    105 | 106 | 107 |
    108 |
    109 | 已登录,注销 110 | 111 | 取消 112 | 115 | 116 |
    117 |
    118 | 122 |
    123 | 124 |
    125 | 126 | -------------------------------------------------------------------------------- /yublog/templates/_pagination.html: -------------------------------------------------------------------------------- 1 | {% macro pages(pagination, cur_page, max_page, endpoint, fragment='') %} 2 |
      3 |
    • 4 | 5 | « 6 | 7 |
    • 8 | {% for p in pagination %} 9 | {% if p %} 10 | {% if p == cur_page %} 11 |
    • 12 | {{ p }} 13 |
    • 14 | {% else %} 15 |
    • 16 | {{ p }} 17 |
    • 18 | {% endif %} 19 | {% else %} 20 |
    • 21 | {% endif %} 22 | {% endfor %} 23 |
    • 24 | 25 | » 26 | 27 |
    • 28 |
    29 | {% endmacro %} -------------------------------------------------------------------------------- /yublog/templates/admin/change_password.html: -------------------------------------------------------------------------------- 1 | {% extends "admin_base.html" %} 2 | {% block tab %}更改密码{% endblock %} 3 | {% block content %} 4 |
    5 |

    更改登录密码

    6 |
    7 |
    8 | {{form.hidden_tag()}} 9 |

    旧密码:{{form.old_password()}}

    10 |

    新密码:{{form.password()}}

    11 |

    确认密码:{{form.password2()}}

    12 |

    13 |
    14 |
    15 |
    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 |
    6 | 29 |
    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 | 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 |
    5 |
    6 | {{form.hidden_tag()}} 7 |
    8 |
    9 |

    允许评论:{{form.enable_comment()}}

    10 |

    导航显示:{{form.show_nav()}}

    11 |

    链接:{{form.url_name(placeholder="url")}}

    12 |

    使用【本地图床】

    13 |
    14 |
    15 |
    16 |
    17 |

    {{form.title(class="aticle-title",placeholder="标题")}}

    18 |
    19 | {{form.body(id="editormd",style="display:block;",rows="5",cols="80")}}
    20 |
    21 |

    {{ form.submit(class="write-btn",value="发布") }}

    22 |
    23 |
    24 |
    25 |
    26 | 27 |
    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 |
    6 | 7 |
    8 | {{form.hidden_tag()}} 9 |
    10 |
    11 |

    分类:{{form.category(placeholder="只能写一个")}}

    12 |

    标签:{{form.tags(placeholder="用英文逗号隔开")}}

    13 |

    链接:{{form.url_name(placeholder="url文章名")}}

    14 |

    日期:{{form.create_time(placeholder="使用-连接年月日")}}

    15 |

    使用【本地图床】

    16 |
    17 |
    18 |
    19 |
    20 |

    {{form.title(class="aticle-title",placeholder="标题")}}

    21 |
    22 | {{form.body(id="editormd",style="display:block;",rows="5",cols="80")}}
    23 |
    24 | {% if title == '写文章' %} 25 |

    {{ form.save_draft(class="write-btn first-btn",value="保存草稿") }}

    26 |

    {{ form.submit(class="write-btn",value="发布") }}

    27 | {% else %} 28 |

    {{ form.save_draft(class="write-btn first-btn",value="保存草稿") }}

    29 |

    {{ form.submit(class="write-btn",value="更新") }}

    30 | {% endif %} 31 |
    32 |
    33 |
    34 |
    35 | 36 |
    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 |
    21 |
    22 | {{form.hidden_tag()}} 23 |
    24 |
    25 |

    勾选广告,盒子会固定顶部

    26 |

    广告:{{form.is_advertising()}}

    27 |
    28 |
    29 |
    30 |
    31 |

    {{form.title(class="aticle-title",placeholder="标题: 可以为空")}}

    32 |
    33 | {{form.body(style="display:block;",rows="5",cols="30",placeholder="支持原生html,css,js。请指定元素宽度不超过240px")}}
    34 |
    35 | {% if title == '添加插件' %} 36 |

    {{ form.submit(class="write-btn",value="添加") }}

    37 | {% else %} 38 |

    {{ form.submit(class="write-btn",value="更新") }}

    39 | {% endif %} 40 |
    41 |
    42 |
    43 |
    44 | 45 |
    46 | {% endblock %} 47 | 48 | -------------------------------------------------------------------------------- /yublog/templates/admin/edit_talk.html: -------------------------------------------------------------------------------- 1 | {% extends "admin_base.html" %} 2 | {% block tab %}写说说{% endblock %} 3 | {% block content %} 4 |
    5 |
    6 | {{form.hidden_tag()}} 7 |
    8 | {{form.talk(placeholder="支持markdown",cols="30",rows="10")}} 9 | 10 |
    11 |
    12 |
    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 | 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 | 39 |
    40 | 41 | -------------------------------------------------------------------------------- /yublog/templates/admin/page.html: -------------------------------------------------------------------------------- 1 | {% extends "admin_base.html" %} 2 | {% block tab %}管理页面{% endblock %} 3 | {% block content %} 4 |
    5 | 18 |
    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 |
    6 | 24 |
    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 |
    5 |

    管理员信息设置

    6 |
    7 |
    8 | {{form.hidden_tag()}} 9 |

    姓名:{{form.username()}}

    10 |

    简介:{{form.profile()}}

    11 |

    网站名:{{form.site_name()}}

    12 |

    网站标题:{{form.site_title()}}

    13 |

    备案信息:{{form.record_info(placeholder="选填")}}

    14 |

    15 |
    16 |
    17 |
    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 |
    7 |

    8 | 9 |
    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 |
    15 | 16 | 71 |
    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 |
    110 |

    111 | 112 |

    113 | 114 |
    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 |
    5 | 21 |
    22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /yublog/templates/admin_column/edit_article.html: -------------------------------------------------------------------------------- 1 | {% extends "admin_base.html" %} 2 | {% block tab %}编辑专栏文章{% endblock %} 3 | {% block content %} 4 |
    5 |
    6 | {{form.hidden_tag()}} 7 |
    8 |
    9 |

    日期:{{form.create_time(placeholder="使用-连接年月日")}}

    10 |

    保密:{{form.secrecy()}}

    11 |

    使用【本地图床】

    12 |
    13 |
    14 |
    15 |
    16 |

    {{form.title(class="aticle-title",placeholder="标题")}}

    17 |
    18 | {{form.body(id="editormd",style="display:block;",rows="5",cols="80")}}
    19 |
    20 | {% if title == '编辑文章' %} 21 |

    {{ form.submit(class="write-btn",value="发布") }}

    22 | {% else %} 23 |

    {{ form.submit(class="write-btn",value="更新") }}

    24 | {% endif %} 25 |
    26 |
    27 |
    28 |
    29 | 30 |
    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 |
    5 |
    6 | {{form.hidden_tag()}} 7 |
    8 |
    9 |

    链接:{{form.url_name(placeholder="url专题名")}}

    10 |

    日期:{{form.create_time(placeholder="使用-连接年月日")}}

    11 |

    密码:{{form.password(placeholder="保密文章的密码")}}

    12 |

    使用【本地图床】

    13 |
    14 |
    15 |
    16 |
    17 |

    {{form.title(class="aticle-title",placeholder="专题名")}}

    18 |
    19 | {{form.body(id="editormd",style="display:block;",rows="5",cols="80")}}
    20 |
    21 | {% if title == '编辑专题' %} 22 |

    {{ form.submit(class="write-btn",value="发布") }}

    23 | {% else %} 24 |

    {{ form.submit(class="write-btn",value="更新") }}

    25 | {% endif %} 26 |
    27 |
    28 |
    29 |
    30 | 31 |
    32 | {% endblock %} 33 | 34 | {% block script %} 35 | {{ super() }} 36 | 37 | 38 | 39 | 48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /yublog/templates/admin_mail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 17 | 26 | 27 | 28 | 29 | 30 | 31 |
    5 | 6 | 7 | 10 | 11 | 12 | 13 |
    8 | 消息 9 | 你的文章《{{ title }} 》有新留言:
    14 |
    18 |

    昵称: {{ nickname }}

    19 |

    网站: {{ website }}

    20 |

    邮箱: {{ email }}

    21 |

    22 | {{ comment | safe}} 23 |

    24 |
    点击查看完整内容
    25 |
    Copyright © YuBlog
    -------------------------------------------------------------------------------- /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 | 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 |
    34 | {{form.hidden_tag()}} 35 |

    36 | {{form.password(placeholder="password")}} 37 |

    38 |

    39 |
    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 | 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 | 404 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 |
    39 | {{form.hidden_tag()}} 40 | {{form.path_name(placeholder="New path.")}} 41 |
    42 |
    43 | {% for path in paths %} 44 |
    45 | 46 | {{ path }} 47 |
    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 |
    101 |
    102 |

    103 |

    104 |

    105 |
    106 |
    107 | 108 |
    109 |
    110 | {% for image in images %} 111 |
    112 | 113 | {{ image.filename }} 114 | 115 |
    116 |

    上传时间:{{ image.timestamp }}

    117 |

    Markdown引用:![{{ image.filename }}](/image/{{ image.path }}/{{ image.filename }})

    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 |
    7 | 17 |
    18 | 19 | {% endblock %} -------------------------------------------------------------------------------- /yublog/templates/main/friends.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 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 |

    21 | 22 | {{ post.title }} 23 | 24 |

    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 | 50 |
    51 | 52 | 72 | 73 |
    74 |
    75 |

    本文作者:俞坤

    76 |

    本文链接:http://www.yukunweb.com/{{post.year}}/{{post.month}}/{{post.url}}

    77 |

    版权声明:本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0许可协议。转载请注明出处!

    78 |
    79 |
    80 | 81 |
    82 | {% if post.prev_post %} 83 | 84 | {{ post.prev_post.title }} 85 | 86 | {% endif %} 87 | {% if post.next_post %} 88 | 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 |
    7 |
      8 | {% for result in results %} 9 |
    • 10 | 11 | {{ result.title }} 12 | 13 |

      14 | {{ result.body_to_html | striptags | truncate(100)}} 15 |

      16 |
    • 17 | {% endfor %} 18 |
    19 |
    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 |
    7 | 17 |
    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 |
    10 |
    11 |

    12 |

    13 |

    14 |
    15 |
    16 |
    17 | {% if images %} 18 | {% for item in images %} 19 |
    20 |
    {{ item.name }}
    21 | 22 | {{ item.name }} 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 | 17 | 18 | 19 | 28 | 29 | 30 | 31 | 32 | 33 |
    5 | 6 | 7 | 8 | 11 | 13 | 14 | 15 |
    9 | 消息 10 | 12 | 您有一条评论回复
    16 |
    20 |

    {{ nickname }}, 你好!

    21 |

    你在该 文章

    22 |

    有新的回复:

    23 |

    24 | {{ comment | safe }} 25 |

    26 |
    点击查看完整内容
    27 |
    Copyright © YuBlog
    -------------------------------------------------------------------------------- /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"{nickname}" 48 | comment_html = f"

    {nickname_p_tag}" \ 49 | f"回复 {reply_author}:

    \n\n {body}" 50 | else: 51 | comment_html = f"

    {nickname}回复" \ 52 | f" {reply_author}:

    \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 | --------------------------------------------------------------------------------