- 新增图像生成接口,支持试用、积分和自定义API Key模式 - 实现生成图片结果异步上传至MinIO存储,带重试机制 - 优化积分预扣除和异常退还逻辑,保障用户积分准确 - 添加获取生成历史记录接口,支持时间范围和分页 - 提供本地字典配置接口,支持模型、比例、提示模板和尺寸 - 实现图片批量上传接口,支持S3兼容对象存储 feat(admin): 增加管理员角色管理与权限分配接口 - 实现角色列表查询、角色创建、更新及删除功能 - 增加权限列表查询接口 - 实现用户角色分配接口,便于统一管理用户权限 - 增加系统字典增删查改接口,支持分类过滤和排序 - 权限控制全面覆盖管理接口,保证安全访问 feat(auth): 完善用户登录注册及权限相关接口与页面 - 实现手机号验证码发送及校验功能,保障注册安全 - 支持手机号注册、登录及退出接口,集成日志记录 - 增加修改密码功能,验证原密码后更新 - 提供动态导航菜单接口,基于权限展示不同菜单 - 实现管理界面路由及日志、角色、字典管理页面访问权限控制 - 添加系统日志查询接口,支持关键词和等级筛选 feat(app): 初始化Flask应用并配置蓝图与数据库 - 创建应用程序工厂,加载配置,初始化数据库和Redis客户端 - 注册认证、API及管理员蓝图,整合路由 - 根路由渲染主页模板 - 应用上下文中自动创建数据库表,保证运行环境准备完毕 feat(database): 提供数据库创建与迁移支持脚本 - 新增数据库创建脚本,支持自动检测是否已存在 - 添加数据库表初始化脚本,支持创建和删除所有表 - 实现RBAC权限初始化,包含基础权限和角色创建 - 新增字段手动修复脚本,添加用户API Key和积分字段 - 强制迁移脚本支持清理连接和修复表结构,初始化默认数据及角色分配 feat(config): 新增系统配置参数 - 配置数据库、Redis、Session和MinIO相关参数 - 添加AI接口地址及试用Key配置 - 集成阿里云短信服务配置及开发模式相关参数 feat(extensions): 初始化数据库、Redis和MinIO客户端 - 创建全局SQLAlchemy数据库实例和Redis客户端 - 配置基于boto3的MinIO兼容S3客户端 chore(logs): 添加示例系统日志文件 - 记录用户请求、验证码发送成功与失败的日志信息
372 lines
12 KiB
Python
372 lines
12 KiB
Python
# Copyright 2012-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"). You
|
|
# may not use this file except in compliance with the License. A copy of
|
|
# the License is located at
|
|
#
|
|
# http://aws.amazon.com/apache2.0/
|
|
#
|
|
# or in the "license" file accompanying this file. This file is
|
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
|
# ANY KIND, either express or implied. See the License for the specific
|
|
# language governing permissions and limitations under the License.
|
|
|
|
import copy
|
|
import datetime
|
|
import sys
|
|
import inspect
|
|
import warnings
|
|
import hashlib
|
|
from http.client import HTTPMessage
|
|
import logging
|
|
import shlex
|
|
import re
|
|
import os
|
|
from collections import OrderedDict
|
|
from collections.abc import MutableMapping
|
|
from math import floor
|
|
|
|
from botocore.vendored import six
|
|
from botocore.exceptions import MD5UnavailableError
|
|
from dateutil.tz import tzlocal
|
|
from urllib3 import exceptions
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class HTTPHeaders(HTTPMessage):
|
|
pass
|
|
|
|
from urllib.parse import (
|
|
quote,
|
|
urlencode,
|
|
unquote,
|
|
unquote_plus,
|
|
urlparse,
|
|
urlsplit,
|
|
urlunsplit,
|
|
urljoin,
|
|
parse_qsl,
|
|
parse_qs,
|
|
)
|
|
from http.client import HTTPResponse
|
|
from io import IOBase as _IOBase
|
|
from base64 import encodebytes
|
|
from email.utils import formatdate
|
|
from itertools import zip_longest
|
|
file_type = _IOBase
|
|
zip = zip
|
|
|
|
# In python3, unquote takes a str() object, url decodes it,
|
|
# then takes the bytestring and decodes it to utf-8.
|
|
unquote_str = unquote_plus
|
|
|
|
def set_socket_timeout(http_response, timeout):
|
|
"""Set the timeout of the socket from an HTTPResponse.
|
|
|
|
:param http_response: An instance of ``httplib.HTTPResponse``
|
|
|
|
"""
|
|
http_response._fp.fp.raw._sock.settimeout(timeout)
|
|
|
|
def accepts_kwargs(func):
|
|
return inspect.getfullargspec(func)[2]
|
|
|
|
def ensure_unicode(s, encoding=None, errors=None):
|
|
# NOOP in Python 3, because every string is already unicode
|
|
return s
|
|
|
|
def ensure_bytes(s, encoding='utf-8', errors='strict'):
|
|
if isinstance(s, str):
|
|
return s.encode(encoding, errors)
|
|
if isinstance(s, bytes):
|
|
return s
|
|
raise ValueError(f"Expected str or bytes, received {type(s)}.")
|
|
|
|
|
|
import xml.etree.ElementTree as ETree
|
|
XMLParseError = ETree.ParseError
|
|
|
|
import json
|
|
|
|
|
|
def filter_ssl_warnings():
|
|
# Ignore warnings related to SNI as it is not being used in validations.
|
|
warnings.filterwarnings(
|
|
'ignore',
|
|
message="A true SSLContext object is not available.*",
|
|
category=exceptions.InsecurePlatformWarning,
|
|
module=r".*urllib3\.util\.ssl_",
|
|
)
|
|
|
|
|
|
@classmethod
|
|
def from_dict(cls, d):
|
|
new_instance = cls()
|
|
for key, value in d.items():
|
|
new_instance[key] = value
|
|
return new_instance
|
|
|
|
|
|
@classmethod
|
|
def from_pairs(cls, pairs):
|
|
new_instance = cls()
|
|
for key, value in pairs:
|
|
new_instance[key] = value
|
|
return new_instance
|
|
|
|
|
|
HTTPHeaders.from_dict = from_dict
|
|
HTTPHeaders.from_pairs = from_pairs
|
|
|
|
|
|
def copy_kwargs(kwargs):
|
|
"""
|
|
This used to be a compat shim for 2.6 but is now just an alias.
|
|
"""
|
|
copy_kwargs = copy.copy(kwargs)
|
|
return copy_kwargs
|
|
|
|
|
|
def total_seconds(delta):
|
|
"""
|
|
Returns the total seconds in a ``datetime.timedelta``.
|
|
|
|
This used to be a compat shim for 2.6 but is now just an alias.
|
|
|
|
:param delta: The timedelta object
|
|
:type delta: ``datetime.timedelta``
|
|
"""
|
|
return delta.total_seconds()
|
|
|
|
|
|
# Checks to see if md5 is available on this system. A given system might not
|
|
# have access to it for various reasons, such as FIPS mode being enabled.
|
|
try:
|
|
hashlib.md5(usedforsecurity=False)
|
|
MD5_AVAILABLE = True
|
|
except (AttributeError, ValueError):
|
|
MD5_AVAILABLE = False
|
|
|
|
|
|
def get_md5(*args, **kwargs):
|
|
"""
|
|
Attempts to get an md5 hashing object.
|
|
|
|
:param args: Args to pass to the MD5 constructor
|
|
:param kwargs: Key word arguments to pass to the MD5 constructor
|
|
:return: An MD5 hashing object if available. If it is unavailable, None
|
|
is returned if raise_error_if_unavailable is set to False.
|
|
"""
|
|
if MD5_AVAILABLE:
|
|
return hashlib.md5(*args, **kwargs)
|
|
else:
|
|
raise MD5UnavailableError()
|
|
|
|
|
|
def compat_shell_split(s, platform=None):
|
|
if platform is None:
|
|
platform = sys.platform
|
|
|
|
if platform == "win32":
|
|
return _windows_shell_split(s)
|
|
else:
|
|
return shlex.split(s)
|
|
|
|
|
|
def _windows_shell_split(s):
|
|
"""Splits up a windows command as the built-in command parser would.
|
|
|
|
Windows has potentially bizarre rules depending on where you look. When
|
|
spawning a process via the Windows C runtime (which is what python does
|
|
when you call popen) the rules are as follows:
|
|
|
|
https://docs.microsoft.com/en-us/cpp/cpp/parsing-cpp-command-line-arguments
|
|
|
|
To summarize:
|
|
|
|
* Only space and tab are valid delimiters
|
|
* Double quotes are the only valid quotes
|
|
* Backslash is interpreted literally unless it is part of a chain that
|
|
leads up to a double quote. Then the backslashes escape the backslashes,
|
|
and if there is an odd number the final backslash escapes the quote.
|
|
|
|
:param s: The command string to split up into parts.
|
|
:return: A list of command components.
|
|
"""
|
|
if not s:
|
|
return []
|
|
|
|
components = []
|
|
buff = []
|
|
is_quoted = False
|
|
num_backslashes = 0
|
|
for character in s:
|
|
if character == '\\':
|
|
# We can't simply append backslashes because we don't know if
|
|
# they are being used as escape characters or not. Instead we
|
|
# keep track of how many we've encountered and handle them when
|
|
# we encounter a different character.
|
|
num_backslashes += 1
|
|
elif character == '"':
|
|
if num_backslashes > 0:
|
|
# The backslashes are in a chain leading up to a double
|
|
# quote, so they are escaping each other.
|
|
buff.append('\\' * int(floor(num_backslashes / 2)))
|
|
remainder = num_backslashes % 2
|
|
num_backslashes = 0
|
|
if remainder == 1:
|
|
# The number of backslashes is uneven, so they are also
|
|
# escaping the double quote, so it needs to be added to
|
|
# the current component buffer.
|
|
buff.append('"')
|
|
continue
|
|
|
|
# We've encountered a double quote that is not escaped,
|
|
# so we toggle is_quoted.
|
|
is_quoted = not is_quoted
|
|
|
|
# If there are quotes, then we may want an empty string. To be
|
|
# safe, we add an empty string to the buffer so that we make
|
|
# sure it sticks around if there's nothing else between quotes.
|
|
# If there is other stuff between quotes, the empty string will
|
|
# disappear during the joining process.
|
|
buff.append('')
|
|
elif character in [' ', '\t'] and not is_quoted:
|
|
# Since the backslashes aren't leading up to a quote, we put in
|
|
# the exact number of backslashes.
|
|
if num_backslashes > 0:
|
|
buff.append('\\' * num_backslashes)
|
|
num_backslashes = 0
|
|
|
|
# Excess whitespace is ignored, so only add the components list
|
|
# if there is anything in the buffer.
|
|
if buff:
|
|
components.append(''.join(buff))
|
|
buff = []
|
|
else:
|
|
# Since the backslashes aren't leading up to a quote, we put in
|
|
# the exact number of backslashes.
|
|
if num_backslashes > 0:
|
|
buff.append('\\' * num_backslashes)
|
|
num_backslashes = 0
|
|
buff.append(character)
|
|
|
|
# Quotes must be terminated.
|
|
if is_quoted:
|
|
raise ValueError(f"No closing quotation in string: {s}")
|
|
|
|
# There may be some leftover backslashes, so we need to add them in.
|
|
# There's no quote so we add the exact number.
|
|
if num_backslashes > 0:
|
|
buff.append('\\' * num_backslashes)
|
|
|
|
# Add the final component in if there is anything in the buffer.
|
|
if buff:
|
|
components.append(''.join(buff))
|
|
|
|
return components
|
|
|
|
|
|
def get_tzinfo_options():
|
|
# Due to dateutil/dateutil#197, Windows may fail to parse times in the past
|
|
# with the system clock. We can alternatively fallback to tzwininfo when
|
|
# this happens, which will get time info from the Windows registry.
|
|
if sys.platform == 'win32':
|
|
from dateutil.tz import tzwinlocal
|
|
|
|
return (tzlocal, tzwinlocal)
|
|
else:
|
|
return (tzlocal,)
|
|
|
|
|
|
# Detect if CRT is available for use
|
|
try:
|
|
import awscrt.auth
|
|
|
|
# Allow user opt-out if needed
|
|
disabled = os.environ.get('BOTO_DISABLE_CRT', "false")
|
|
HAS_CRT = not disabled.lower() == 'true'
|
|
except ImportError:
|
|
HAS_CRT = False
|
|
|
|
|
|
def has_minimum_crt_version(minimum_version):
|
|
"""Not intended for use outside botocore."""
|
|
if not HAS_CRT:
|
|
return False
|
|
|
|
crt_version_str = awscrt.__version__
|
|
try:
|
|
crt_version_ints = map(int, crt_version_str.split("."))
|
|
crt_version_tuple = tuple(crt_version_ints)
|
|
except (TypeError, ValueError):
|
|
return False
|
|
|
|
return crt_version_tuple >= minimum_version
|
|
|
|
|
|
def get_current_datetime(remove_tzinfo=True):
|
|
"""Retrieve the current timezone in UTC, with or without an explicit timezone."""
|
|
datetime_now = datetime.datetime.now(datetime.timezone.utc)
|
|
if remove_tzinfo:
|
|
datetime_now = datetime_now.replace(tzinfo=None)
|
|
return datetime_now
|
|
|
|
|
|
########################################################
|
|
# urllib3 compat backports #
|
|
########################################################
|
|
|
|
# Vendoring IPv6 validation regex patterns from urllib3
|
|
# https://github.com/urllib3/urllib3/blob/7e856c0/src/urllib3/util/url.py
|
|
IPV4_PAT = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}"
|
|
IPV4_RE = re.compile("^" + IPV4_PAT + "$")
|
|
HEX_PAT = "[0-9A-Fa-f]{1,4}"
|
|
LS32_PAT = "(?:{hex}:{hex}|{ipv4})".format(hex=HEX_PAT, ipv4=IPV4_PAT)
|
|
_subs = {"hex": HEX_PAT, "ls32": LS32_PAT}
|
|
_variations = [
|
|
# 6( h16 ":" ) ls32
|
|
"(?:%(hex)s:){6}%(ls32)s",
|
|
# "::" 5( h16 ":" ) ls32
|
|
"::(?:%(hex)s:){5}%(ls32)s",
|
|
# [ h16 ] "::" 4( h16 ":" ) ls32
|
|
"(?:%(hex)s)?::(?:%(hex)s:){4}%(ls32)s",
|
|
# [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
|
|
"(?:(?:%(hex)s:)?%(hex)s)?::(?:%(hex)s:){3}%(ls32)s",
|
|
# [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
|
|
"(?:(?:%(hex)s:){0,2}%(hex)s)?::(?:%(hex)s:){2}%(ls32)s",
|
|
# [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32
|
|
"(?:(?:%(hex)s:){0,3}%(hex)s)?::%(hex)s:%(ls32)s",
|
|
# [ *4( h16 ":" ) h16 ] "::" ls32
|
|
"(?:(?:%(hex)s:){0,4}%(hex)s)?::%(ls32)s",
|
|
# [ *5( h16 ":" ) h16 ] "::" h16
|
|
"(?:(?:%(hex)s:){0,5}%(hex)s)?::%(hex)s",
|
|
# [ *6( h16 ":" ) h16 ] "::"
|
|
"(?:(?:%(hex)s:){0,6}%(hex)s)?::",
|
|
]
|
|
|
|
UNRESERVED_PAT = (
|
|
r"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._!\-~"
|
|
)
|
|
IPV6_PAT = "(?:" + "|".join([x % _subs for x in _variations]) + ")"
|
|
ZONE_ID_PAT = "(?:%25|%)(?:[" + UNRESERVED_PAT + "]|%[a-fA-F0-9]{2})+"
|
|
IPV6_ADDRZ_PAT = r"\[" + IPV6_PAT + r"(?:" + ZONE_ID_PAT + r")?\]"
|
|
IPV6_ADDRZ_RE = re.compile("^" + IPV6_ADDRZ_PAT + "$")
|
|
|
|
# These are the characters that are stripped by post-bpo-43882 urlparse().
|
|
UNSAFE_URL_CHARS = frozenset('\t\r\n')
|
|
|
|
# Detect if gzip is available for use
|
|
try:
|
|
import gzip
|
|
HAS_GZIP = True
|
|
except ImportError:
|
|
HAS_GZIP = False
|
|
|
|
# Conditional import for awscrt EC crypto functionality
|
|
if HAS_CRT and has_minimum_crt_version((0, 28, 4)):
|
|
from awscrt.crypto import EC
|
|
else:
|
|
EC = None
|