- 新增图像生成接口,支持试用、积分和自定义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): 添加示例系统日志文件 - 记录用户请求、验证码发送成功与失败的日志信息
288 lines
9.8 KiB
Python
288 lines
9.8 KiB
Python
# Copyright (c) 2012-2013 Mitch Garnaat http://garnaat.org/
|
|
# Copyright 2012-2016 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 configparser
|
|
import copy
|
|
import os
|
|
import shlex
|
|
import sys
|
|
|
|
import botocore.exceptions
|
|
|
|
|
|
def multi_file_load_config(*filenames):
|
|
"""Load and combine multiple INI configs with profiles.
|
|
|
|
This function will take a list of filesnames and return
|
|
a single dictionary that represents the merging of the loaded
|
|
config files.
|
|
|
|
If any of the provided filenames does not exist, then that file
|
|
is ignored. It is therefore ok to provide a list of filenames,
|
|
some of which may not exist.
|
|
|
|
Configuration files are **not** deep merged, only the top level
|
|
keys are merged. The filenames should be passed in order of
|
|
precedence. The first config file has precedence over the
|
|
second config file, which has precedence over the third config file,
|
|
etc. The only exception to this is that the "profiles" key is
|
|
merged to combine profiles from multiple config files into a
|
|
single profiles mapping. However, if a profile is defined in
|
|
multiple config files, then the config file with the highest
|
|
precedence is used. Profile values themselves are not merged.
|
|
For example::
|
|
|
|
FileA FileB FileC
|
|
[foo] [foo] [bar]
|
|
a=1 a=2 a=3
|
|
b=2
|
|
|
|
[bar] [baz] [profile a]
|
|
a=2 a=3 region=e
|
|
|
|
[profile a] [profile b] [profile c]
|
|
region=c region=d region=f
|
|
|
|
The final result of ``multi_file_load_config(FileA, FileB, FileC)``
|
|
would be::
|
|
|
|
{"foo": {"a": 1}, "bar": {"a": 2}, "baz": {"a": 3},
|
|
"profiles": {"a": {"region": "c"}}, {"b": {"region": d"}},
|
|
{"c": {"region": "f"}}}
|
|
|
|
Note that the "foo" key comes from A, even though it's defined in both
|
|
FileA and FileB. Because "foo" was defined in FileA first, then the values
|
|
for "foo" from FileA are used and the values for "foo" from FileB are
|
|
ignored. Also note where the profiles originate from. Profile "a"
|
|
comes FileA, profile "b" comes from FileB, and profile "c" comes
|
|
from FileC.
|
|
|
|
"""
|
|
configs = []
|
|
profiles = []
|
|
for filename in filenames:
|
|
try:
|
|
loaded = load_config(filename)
|
|
except botocore.exceptions.ConfigNotFound:
|
|
continue
|
|
profiles.append(loaded.pop('profiles'))
|
|
configs.append(loaded)
|
|
merged_config = _merge_list_of_dicts(configs)
|
|
merged_profiles = _merge_list_of_dicts(profiles)
|
|
merged_config['profiles'] = merged_profiles
|
|
return merged_config
|
|
|
|
|
|
def _merge_list_of_dicts(list_of_dicts):
|
|
merged_dicts = {}
|
|
for single_dict in list_of_dicts:
|
|
for key, value in single_dict.items():
|
|
if key not in merged_dicts:
|
|
merged_dicts[key] = value
|
|
return merged_dicts
|
|
|
|
|
|
def load_config(config_filename):
|
|
"""Parse a INI config with profiles.
|
|
|
|
This will parse an INI config file and map top level profiles
|
|
into a top level "profile" key.
|
|
|
|
If you want to parse an INI file and map all section names to
|
|
top level keys, use ``raw_config_parse`` instead.
|
|
|
|
"""
|
|
parsed = raw_config_parse(config_filename)
|
|
return build_profile_map(parsed)
|
|
|
|
|
|
def raw_config_parse(config_filename, parse_subsections=True):
|
|
"""Returns the parsed INI config contents.
|
|
|
|
Each section name is a top level key.
|
|
|
|
:param config_filename: The name of the INI file to parse
|
|
|
|
:param parse_subsections: If True, parse indented blocks as
|
|
subsections that represent their own configuration dictionary.
|
|
For example, if the config file had the contents::
|
|
|
|
s3 =
|
|
signature_version = s3v4
|
|
addressing_style = path
|
|
|
|
The resulting ``raw_config_parse`` would be::
|
|
|
|
{'s3': {'signature_version': 's3v4', 'addressing_style': 'path'}}
|
|
|
|
If False, do not try to parse subsections and return the indented
|
|
block as its literal value::
|
|
|
|
{'s3': '\nsignature_version = s3v4\naddressing_style = path'}
|
|
|
|
:returns: A dict with keys for each profile found in the config
|
|
file and the value of each key being a dict containing name
|
|
value pairs found in that profile.
|
|
|
|
:raises: ConfigNotFound, ConfigParseError
|
|
"""
|
|
config = {}
|
|
path = config_filename
|
|
if path is not None:
|
|
path = os.path.expandvars(path)
|
|
path = os.path.expanduser(path)
|
|
if not os.path.isfile(path):
|
|
raise botocore.exceptions.ConfigNotFound(path=_unicode_path(path))
|
|
cp = configparser.RawConfigParser()
|
|
try:
|
|
cp.read([path])
|
|
except (configparser.Error, UnicodeDecodeError) as e:
|
|
raise botocore.exceptions.ConfigParseError(
|
|
path=_unicode_path(path), error=e
|
|
) from None
|
|
else:
|
|
for section in cp.sections():
|
|
config[section] = {}
|
|
for option in cp.options(section):
|
|
config_value = cp.get(section, option)
|
|
if parse_subsections and config_value.startswith('\n'):
|
|
# Then we need to parse the inner contents as
|
|
# hierarchical. We support a single level
|
|
# of nesting for now.
|
|
try:
|
|
config_value = _parse_nested(config_value)
|
|
except ValueError as e:
|
|
raise botocore.exceptions.ConfigParseError(
|
|
path=_unicode_path(path), error=e
|
|
) from None
|
|
config[section][option] = config_value
|
|
return config
|
|
|
|
|
|
def _unicode_path(path):
|
|
if isinstance(path, str):
|
|
return path
|
|
# According to the documentation getfilesystemencoding can return None
|
|
# on unix in which case the default encoding is used instead.
|
|
filesystem_encoding = sys.getfilesystemencoding()
|
|
if filesystem_encoding is None:
|
|
filesystem_encoding = sys.getdefaultencoding()
|
|
return path.decode(filesystem_encoding, 'replace')
|
|
|
|
|
|
def _parse_nested(config_value):
|
|
# Given a value like this:
|
|
# \n
|
|
# foo = bar
|
|
# bar = baz
|
|
# We need to parse this into
|
|
# {'foo': 'bar', 'bar': 'baz}
|
|
parsed = {}
|
|
for line in config_value.splitlines():
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
# The caller will catch ValueError
|
|
# and raise an appropriate error
|
|
# if this fails.
|
|
key, value = line.split('=', 1)
|
|
parsed[key.strip()] = value.strip()
|
|
return parsed
|
|
|
|
|
|
def _parse_section(key, values):
|
|
result = {}
|
|
try:
|
|
parts = shlex.split(key)
|
|
except ValueError:
|
|
return result
|
|
if len(parts) == 2:
|
|
result[parts[1]] = values
|
|
return result
|
|
|
|
|
|
def build_profile_map(parsed_ini_config):
|
|
"""Convert the parsed INI config into a profile map.
|
|
|
|
The config file format requires that every profile except the
|
|
default to be prepended with "profile", e.g.::
|
|
|
|
[profile test]
|
|
aws_... = foo
|
|
aws_... = bar
|
|
|
|
[profile bar]
|
|
aws_... = foo
|
|
aws_... = bar
|
|
|
|
# This is *not* a profile
|
|
[preview]
|
|
otherstuff = 1
|
|
|
|
# Neither is this
|
|
[foobar]
|
|
morestuff = 2
|
|
|
|
The build_profile_map will take a parsed INI config file where each top
|
|
level key represents a section name, and convert into a format where all
|
|
the profiles are under a single top level "profiles" key, and each key in
|
|
the sub dictionary is a profile name. For example, the above config file
|
|
would be converted from::
|
|
|
|
{"profile test": {"aws_...": "foo", "aws...": "bar"},
|
|
"profile bar": {"aws...": "foo", "aws...": "bar"},
|
|
"preview": {"otherstuff": ...},
|
|
"foobar": {"morestuff": ...},
|
|
}
|
|
|
|
into::
|
|
|
|
{"profiles": {"test": {"aws_...": "foo", "aws...": "bar"},
|
|
"bar": {"aws...": "foo", "aws...": "bar"},
|
|
"preview": {"otherstuff": ...},
|
|
"foobar": {"morestuff": ...},
|
|
}
|
|
|
|
If there are no profiles in the provided parsed INI contents, then
|
|
an empty dict will be the value associated with the ``profiles`` key.
|
|
|
|
.. note::
|
|
|
|
This will not mutate the passed in parsed_ini_config. Instead it will
|
|
make a deepcopy and return that value.
|
|
|
|
"""
|
|
parsed_config = copy.deepcopy(parsed_ini_config)
|
|
profiles = {}
|
|
sso_sessions = {}
|
|
services = {}
|
|
final_config = {}
|
|
for key, values in parsed_config.items():
|
|
if key.startswith("profile"):
|
|
profiles.update(_parse_section(key, values))
|
|
elif key.startswith("sso-session"):
|
|
sso_sessions.update(_parse_section(key, values))
|
|
elif key.startswith("services"):
|
|
services.update(_parse_section(key, values))
|
|
elif key == 'default':
|
|
# default section is special and is considered a profile
|
|
# name but we don't require you use 'profile "default"'
|
|
# as a section.
|
|
profiles[key] = values
|
|
else:
|
|
final_config[key] = values
|
|
final_config['profiles'] = profiles
|
|
final_config['sso_sessions'] = sso_sessions
|
|
final_config['services'] = services
|
|
return final_config
|